Bug 14937: Expiration date for holds based onReservesMaxPickUpDelay should not requir...
[koha.git] / C4 / Letters.pm
1 package C4::Letters;
2
3 # Copyright 2000-2002 Katipo Communications
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22
23 use MIME::Lite;
24 use Mail::Sendmail;
25
26 use C4::Koha qw(GetAuthorisedValueByCode);
27 use C4::Members;
28 use C4::Members::Attributes qw(GetBorrowerAttributes);
29 use C4::Branch;
30 use C4::Log;
31 use C4::SMS;
32 use C4::Debug;
33 use Koha::DateUtils;
34 use Date::Calc qw( Add_Delta_Days );
35 use Encode;
36 use Carp;
37 use Koha::Email;
38 use Koha::DateUtils qw( format_sqldatetime );
39
40 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
41
42 BEGIN {
43     require Exporter;
44     # set the version for version checking
45     $VERSION = 3.07.00.049;
46     @ISA = qw(Exporter);
47     @EXPORT = qw(
48         &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &addalert &getalert &delalert &findrelatedto &SendAlerts &GetPrintMessages &GetMessageTransportTypes
49     );
50 }
51
52 =head1 NAME
53
54 C4::Letters - Give functions for Letters management
55
56 =head1 SYNOPSIS
57
58   use C4::Letters;
59
60 =head1 DESCRIPTION
61
62   "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
63   late issues, as well as other tasks like sending a mail to users that have subscribed to a "serial issue alert" (= being warned every time a new issue has arrived at the library)
64
65   Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
66
67 =head2 GetLetters([$module])
68
69   $letters = &GetLetters($module);
70   returns informations about letters.
71   if needed, $module filters for letters given module
72
73 =cut
74
75 sub GetLetters {
76     my ($filters) = @_;
77     my $module    = $filters->{module};
78     my $code      = $filters->{code};
79     my $branchcode = $filters->{branchcode};
80     my $dbh       = C4::Context->dbh;
81     my $letters   = $dbh->selectall_arrayref(
82         q|
83             SELECT module, code, branchcode, name
84             FROM letter
85             WHERE 1
86         |
87           . ( $module ? q| AND module = ?| : q|| )
88           . ( $code   ? q| AND code = ?|   : q|| )
89           . ( defined $branchcode   ? q| AND branchcode = ?|   : q|| )
90           . q| GROUP BY code ORDER BY name|, { Slice => {} }
91         , ( $module ? $module : () )
92         , ( $code ? $code : () )
93         , ( defined $branchcode ? $branchcode : () )
94     );
95
96     return $letters;
97 }
98
99 =head2 GetLetterTemplates
100
101     my $letter_templates = GetLetterTemplates(
102         {
103             module => 'circulation',
104             code => 'my code',
105             branchcode => 'CPL', # '' for default,
106         }
107     );
108
109     Return a hashref of letter templates.
110     The key will be the message transport type.
111
112 =cut
113
114 sub GetLetterTemplates {
115     my ( $params ) = @_;
116
117     my $module    = $params->{module};
118     my $code      = $params->{code};
119     my $branchcode = $params->{branchcode} // '';
120     my $dbh       = C4::Context->dbh;
121     my $letters   = $dbh->selectall_hashref(
122         q|
123             SELECT module, code, branchcode, name, is_html, title, content, message_transport_type
124             FROM letter
125             WHERE module = ?
126             AND code = ?
127             and branchcode = ?
128         |
129         , 'message_transport_type'
130         , undef
131         , $module, $code, $branchcode
132     );
133
134     return $letters;
135 }
136
137 =head2 GetLettersAvailableForALibrary
138
139     my $letters = GetLettersAvailableForALibrary(
140         {
141             branchcode => 'CPL', # '' for default
142             module => 'circulation',
143         }
144     );
145
146     Return an arrayref of letters, sorted by name.
147     If a specific letter exist for the given branchcode, it will be retrieve.
148     Otherwise the default letter will be.
149
150 =cut
151
152 sub GetLettersAvailableForALibrary {
153     my ($filters)  = @_;
154     my $branchcode = $filters->{branchcode};
155     my $module     = $filters->{module};
156
157     croak "module should be provided" unless $module;
158
159     my $dbh             = C4::Context->dbh;
160     my $default_letters = $dbh->selectall_arrayref(
161         q|
162             SELECT module, code, branchcode, name
163             FROM letter
164             WHERE 1
165         |
166           . q| AND branchcode = ''|
167           . ( $module ? q| AND module = ?| : q|| )
168           . q| ORDER BY name|, { Slice => {} }
169         , ( $module ? $module : () )
170     );
171
172     my $specific_letters;
173     if ($branchcode) {
174         $specific_letters = $dbh->selectall_arrayref(
175             q|
176                 SELECT module, code, branchcode, name
177                 FROM letter
178                 WHERE 1
179             |
180               . q| AND branchcode = ?|
181               . ( $module ? q| AND module = ?| : q|| )
182               . q| ORDER BY name|, { Slice => {} }
183             , $branchcode
184             , ( $module ? $module : () )
185         );
186     }
187
188     my %letters;
189     for my $l (@$default_letters) {
190         $letters{ $l->{code} } = $l;
191     }
192     for my $l (@$specific_letters) {
193         # Overwrite the default letter with the specific one.
194         $letters{ $l->{code} } = $l;
195     }
196
197     return [ map { $letters{$_} }
198           sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
199           keys %letters ];
200
201 }
202
203 # FIXME: using our here means that a Plack server will need to be
204 #        restarted fairly regularly when working with this routine.
205 #        A better option would be to use Koha::Cache and use a cache
206 #        that actually works in a persistent environment, but as a
207 #        short-term fix, our will work.
208 our %letter;
209 sub getletter {
210     my ( $module, $code, $branchcode, $message_transport_type ) = @_;
211     $message_transport_type //= '%';
212
213     if ( C4::Context->preference('IndependentBranches')
214             and $branchcode
215             and C4::Context->userenv ) {
216
217         $branchcode = C4::Context->userenv->{'branch'};
218     }
219     $branchcode //= '';
220
221     if ( my $l = $letter{$module}{$code}{$branchcode}{$message_transport_type} ) {
222         return { %$l }; # deep copy
223     }
224
225     my $dbh = C4::Context->dbh;
226     my $sth = $dbh->prepare(q{
227         SELECT *
228         FROM letter
229         WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
230         AND message_transport_type LIKE ?
231         ORDER BY branchcode DESC LIMIT 1
232     });
233     $sth->execute( $module, $code, $branchcode, $message_transport_type );
234     my $line = $sth->fetchrow_hashref
235       or return;
236     $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
237     $letter{$module}{$code}{$branchcode}{$message_transport_type} = $line;
238     return { %$line };
239 }
240
241
242 =head2 DelLetter
243
244     DelLetter(
245         {
246             branchcode => 'CPL',
247             module => 'circulation',
248             code => 'my code',
249             [ mtt => 'email', ]
250         }
251     );
252
253     Delete the letter. The mtt parameter is facultative.
254     If not given, all templates mathing the other parameters will be removed.
255
256 =cut
257
258 sub DelLetter {
259     my ($params)   = @_;
260     my $branchcode = $params->{branchcode};
261     my $module     = $params->{module};
262     my $code       = $params->{code};
263     my $mtt        = $params->{mtt};
264     my $dbh        = C4::Context->dbh;
265     $dbh->do(q|
266         DELETE FROM letter
267         WHERE branchcode = ?
268           AND module = ?
269           AND code = ?
270     | . ( $mtt ? q| AND message_transport_type = ?| : q|| )
271     , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ) );
272 }
273
274 =head2 addalert ($borrowernumber, $type, $externalid)
275
276     parameters : 
277     - $borrowernumber : the number of the borrower subscribing to the alert
278     - $type : the type of alert.
279     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
280     
281     create an alert and return the alertid (primary key)
282
283 =cut
284
285 sub addalert {
286     my ( $borrowernumber, $type, $externalid ) = @_;
287     my $dbh = C4::Context->dbh;
288     my $sth =
289       $dbh->prepare(
290         "insert into alert (borrowernumber, type, externalid) values (?,?,?)");
291     $sth->execute( $borrowernumber, $type, $externalid );
292
293     # get the alert number newly created and return it
294     my $alertid = $dbh->{'mysql_insertid'};
295     return $alertid;
296 }
297
298 =head2 delalert ($alertid)
299
300     parameters :
301     - alertid : the alert id
302     deletes the alert
303
304 =cut
305
306 sub delalert {
307     my $alertid = shift or die "delalert() called without valid argument (alertid)";    # it's gonna die anyway.
308     $debug and warn "delalert: deleting alertid $alertid";
309     my $sth = C4::Context->dbh->prepare("delete from alert where alertid=?");
310     $sth->execute($alertid);
311 }
312
313 =head2 getalert ([$borrowernumber], [$type], [$externalid])
314
315     parameters :
316     - $borrowernumber : the number of the borrower subscribing to the alert
317     - $type : the type of alert.
318     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
319     all parameters NON mandatory. If a parameter is omitted, the query is done without the corresponding parameter. For example, without $externalid, returns all alerts for a borrower on a topic.
320
321 =cut
322
323 sub getalert {
324     my ( $borrowernumber, $type, $externalid ) = @_;
325     my $dbh   = C4::Context->dbh;
326     my $query = "SELECT a.*, b.branchcode FROM alert a JOIN borrowers b USING(borrowernumber) WHERE";
327     my @bind;
328     if ($borrowernumber and $borrowernumber =~ /^\d+$/) {
329         $query .= " borrowernumber=? AND ";
330         push @bind, $borrowernumber;
331     }
332     if ($type) {
333         $query .= " type=? AND ";
334         push @bind, $type;
335     }
336     if ($externalid) {
337         $query .= " externalid=? AND ";
338         push @bind, $externalid;
339     }
340     $query =~ s/ AND $//;
341     my $sth = $dbh->prepare($query);
342     $sth->execute(@bind);
343     return $sth->fetchall_arrayref({});
344 }
345
346 =head2 findrelatedto($type, $externalid)
347
348     parameters :
349     - $type : the type of alert
350     - $externalid : the id of the "object" to query
351
352     In the table alert, a "id" is stored in the externalid field. This "id" is related to another table, depending on the type of the alert.
353     When type=issue, the id is related to a subscriptionid and this sub returns the name of the biblio.
354
355 =cut
356     
357 # outmoded POD:
358 # When type=virtual, the id is related to a virtual shelf and this sub returns the name of the sub
359
360 sub findrelatedto {
361     my $type       = shift or return;
362     my $externalid = shift or return;
363     my $q = ($type eq 'issue'   ) ?
364 "select title as result from subscription left join biblio on subscription.biblionumber=biblio.biblionumber where subscriptionid=?" :
365             ($type eq 'borrower') ?
366 "select concat(firstname,' ',surname) from borrowers where borrowernumber=?" : undef;
367     unless ($q) {
368         warn "findrelatedto(): Illegal type '$type'";
369         return;
370     }
371     my $sth = C4::Context->dbh->prepare($q);
372     $sth->execute($externalid);
373     my ($result) = $sth->fetchrow;
374     return $result;
375 }
376
377 =head2 SendAlerts
378
379     parameters :
380     - $type : the type of alert
381     - $externalid : the id of the "object" to query
382     - $letter_code : the letter to send.
383
384     send an alert to all borrowers having put an alert on a given subject.
385
386 =cut
387
388 sub SendAlerts {
389     my ( $type, $externalid, $letter_code ) = @_;
390     my $dbh = C4::Context->dbh;
391     if ( $type eq 'issue' ) {
392
393         # prepare the letter...
394         # search the subscriptionid
395         my $sth =
396           $dbh->prepare(
397             "SELECT subscriptionid FROM serial WHERE serialid=?");
398         $sth->execute($externalid);
399         my ($subscriptionid) = $sth->fetchrow
400           or warn( "No subscription for '$externalid'" ),
401              return;
402
403         # search the biblionumber
404         $sth =
405           $dbh->prepare(
406             "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
407         $sth->execute($subscriptionid);
408         my ($biblionumber) = $sth->fetchrow
409           or warn( "No biblionumber for '$subscriptionid'" ),
410              return;
411
412         my %letter;
413         # find the list of borrowers to alert
414         my $alerts = getalert( '', 'issue', $subscriptionid );
415         foreach (@$alerts) {
416             my $borinfo = C4::Members::GetMember('borrowernumber' => $_->{'borrowernumber'});
417             my $email = $borinfo->{email} or next;
418
419 #                    warn "sending issues...";
420             my $userenv = C4::Context->userenv;
421             my $branchdetails = GetBranchDetail($_->{'branchcode'});
422             my $letter = GetPreparedLetter (
423                 module => 'serial',
424                 letter_code => $letter_code,
425                 branchcode => $userenv->{branch},
426                 tables => {
427                     'branches'    => $_->{branchcode},
428                     'biblio'      => $biblionumber,
429                     'biblioitems' => $biblionumber,
430                     'borrowers'   => $borinfo,
431                     'subscription' => $subscriptionid,
432                     'serial' => $externalid,
433                 },
434                 want_librarian => 1,
435             ) or return;
436
437             # ... then send mail
438             my $message = Koha::Email->new();
439             my %mail = $message->create_message_headers(
440                 {
441                     to      => $email,
442                     from    => $branchdetails->{'branchemail'},
443                     replyto => $branchdetails->{'branchreplyto'},
444                     sender  => $branchdetails->{'branchreturnpath'},
445                     subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
446                     message => $letter->{'is_html'}
447                                 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
448                                               Encode::encode( "UTF-8", "" . $letter->{'title'} ))
449                                 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
450                     contenttype => $letter->{'is_html'}
451                                     ? 'text/html; charset="utf-8"'
452                                     : 'text/plain; charset="utf-8"',
453                 }
454             );
455             sendmail(%mail) or carp $Mail::Sendmail::error;
456         }
457     }
458     elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' ) {
459
460         # prepare the letter...
461         # search the biblionumber
462         my $strsth =  $type eq 'claimacquisition'
463             ? qq{
464             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
465             FROM aqorders
466             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
467             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
468             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
469             WHERE aqorders.ordernumber IN (
470             }
471             : qq{
472             SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
473             aqbooksellers.id AS booksellerid
474             FROM serial
475             LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
476             LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
477             LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
478             WHERE serial.serialid IN (
479             };
480
481         if (!@$externalid){
482             carp "No Order seleted";
483             return { error => "no_order_seleted" };
484         }
485
486         $strsth .= join( ",", @$externalid ) . ")";
487         my $sthorders = $dbh->prepare($strsth);
488         $sthorders->execute;
489         my $dataorders = $sthorders->fetchall_arrayref( {} );
490
491         my $sthbookseller =
492           $dbh->prepare("select * from aqbooksellers where id=?");
493         $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
494         my $databookseller = $sthbookseller->fetchrow_hashref;
495         my $addressee =  $type eq 'claimacquisition' ? 'acqprimary' : 'serialsprimary';
496         my $sthcontact =
497           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
498         $sthcontact->execute( $dataorders->[0]->{booksellerid} );
499         my $datacontact = $sthcontact->fetchrow_hashref;
500
501         my @email;
502         my @cc;
503         push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
504         push @email, $datacontact->{email}           if ( $datacontact && $datacontact->{email} );
505         unless (@email) {
506             warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
507             return { error => "no_email" };
508         }
509         my $addlcontact;
510         while ($addlcontact = $sthcontact->fetchrow_hashref) {
511             push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
512         }
513
514         my $userenv = C4::Context->userenv;
515         my $letter = GetPreparedLetter (
516             module => $type,
517             letter_code => $letter_code,
518             branchcode => $userenv->{branch},
519             tables => {
520                 'branches'    => $userenv->{branch},
521                 'aqbooksellers' => $databookseller,
522                 'aqcontacts'    => $datacontact,
523             },
524             repeat => $dataorders,
525             want_librarian => 1,
526         ) or return;
527
528         # Remove the order tag
529         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
530
531         # ... then send mail
532         my %mail = (
533             To => join( ',', @email),
534             Cc             => join( ',', @cc),
535             From           => $userenv->{emailaddress},
536             Subject        => Encode::encode( "UTF-8", "" . $letter->{title} ),
537             Message => $letter->{'is_html'}
538                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
539                                           Encode::encode( "UTF-8", "" . $letter->{'title'} ))
540                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
541             'Content-Type' => $letter->{'is_html'}
542                                 ? 'text/html; charset="utf-8"'
543                                 : 'text/plain; charset="utf-8"',
544         );
545
546         $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
547           if C4::Context->preference('ReplytoDefault');
548         $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
549           if C4::Context->preference('ReturnpathDefault');
550
551         unless ( sendmail(%mail) ) {
552             carp $Mail::Sendmail::error;
553             return { error => $Mail::Sendmail::error };
554         }
555
556         logaction(
557             "ACQUISITION",
558             $type eq 'claimissues' ? "CLAIM ISSUE" : "ACQUISITION CLAIM",
559             undef,
560             "To="
561                 . join( ',', @email )
562                 . " Title="
563                 . $letter->{title}
564                 . " Content="
565                 . $letter->{content}
566         ) if C4::Context->preference("LetterLog");
567     }
568    # send an "account details" notice to a newly created user
569     elsif ( $type eq 'members' ) {
570         my $branchdetails = GetBranchDetail($externalid->{'branchcode'});
571         my $letter = GetPreparedLetter (
572             module => 'members',
573             letter_code => $letter_code,
574             branchcode => $externalid->{'branchcode'},
575             tables => {
576                 'branches'    => $branchdetails,
577                 'borrowers' => $externalid->{'borrowernumber'},
578             },
579             substitute => { 'borrowers.password' => $externalid->{'password'} },
580             want_librarian => 1,
581         ) or return;
582         return { error => "no_email" } unless $externalid->{'emailaddr'};
583         my $email = Koha::Email->new();
584         my %mail  = $email->create_message_headers(
585             {
586                 to      => $externalid->{'emailaddr'},
587                 from    => $branchdetails->{'branchemail'},
588                 replyto => $branchdetails->{'branchreplyto'},
589                 sender  => $branchdetails->{'branchreturnpath'},
590                 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
591                 message => $letter->{'is_html'}
592                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
593                                           Encode::encode( "UTF-8", "" . $letter->{'title'}  ) )
594                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
595                 contenttype => $letter->{'is_html'}
596                                 ? 'text/html; charset="utf-8"'
597                                 : 'text/plain; charset="utf-8"',
598             }
599         );
600         sendmail(%mail) or carp $Mail::Sendmail::error;
601     }
602 }
603
604 =head2 GetPreparedLetter( %params )
605
606     %params hash:
607       module => letter module, mandatory
608       letter_code => letter code, mandatory
609       branchcode => for letter selection, if missing default system letter taken
610       tables => a hashref with table names as keys. Values are either:
611         - a scalar - primary key value
612         - an arrayref - primary key values
613         - a hashref - full record
614       substitute => custom substitution key/value pairs
615       repeat => records to be substituted on consecutive lines:
616         - an arrayref - tries to guess what needs substituting by
617           taking remaining << >> tokensr; not recommended
618         - a hashref token => @tables - replaces <token> << >> << >> </token>
619           subtemplate for each @tables row; table is a hashref as above
620       want_librarian => boolean,  if set to true triggers librarian details
621         substitution from the userenv
622     Return value:
623       letter fields hashref (title & content useful)
624
625 =cut
626
627 sub GetPreparedLetter {
628     my %params = @_;
629
630     my $module      = $params{module} or croak "No module";
631     my $letter_code = $params{letter_code} or croak "No letter_code";
632     my $branchcode  = $params{branchcode} || '';
633     my $mtt         = $params{message_transport_type} || 'email';
634
635     my $letter = getletter( $module, $letter_code, $branchcode, $mtt )
636         or warn( "No $module $letter_code letter transported by " . $mtt ),
637             return;
638
639     my $tables = $params{tables};
640     my $substitute = $params{substitute};
641     my $repeat = $params{repeat};
642     $tables || $substitute || $repeat
643       or carp( "ERROR: nothing to substitute - both 'tables' and 'substitute' are empty" ),
644          return;
645     my $want_librarian = $params{want_librarian};
646
647     if ($substitute) {
648         while ( my ($token, $val) = each %$substitute ) {
649             if ( $token eq 'items.content' ) {
650                 $val =~ s|\n|<br/>|g if $letter->{is_html};
651             }
652
653             $letter->{title} =~ s/<<$token>>/$val/g;
654             $letter->{content} =~ s/<<$token>>/$val/g;
655        }
656     }
657
658     my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
659     $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
660
661     if ($want_librarian) {
662         # parsing librarian name
663         my $userenv = C4::Context->userenv;
664         $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
665         $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
666         $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
667     }
668
669     my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
670
671     if ($repeat) {
672         if (ref ($repeat) eq 'ARRAY' ) {
673             $repeat_no_enclosing_tags = $repeat;
674         } else {
675             $repeat_enclosing_tags = $repeat;
676         }
677     }
678
679     if ($repeat_enclosing_tags) {
680         while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
681             if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
682                 my $subcontent = $1;
683                 my @lines = map {
684                     my %subletter = ( title => '', content => $subcontent );
685                     _substitute_tables( \%subletter, $_ );
686                     $subletter{content};
687                 } @$tag_tables;
688                 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
689             }
690         }
691     }
692
693     if ($tables) {
694         _substitute_tables( $letter, $tables );
695     }
696
697     if ($repeat_no_enclosing_tags) {
698         if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
699             my $line = $&;
700             my $i = 1;
701             my @lines = map {
702                 my $c = $line;
703                 $c =~ s/<<count>>/$i/go;
704                 foreach my $field ( keys %{$_} ) {
705                     $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
706                 }
707                 $i++;
708                 $c;
709             } @$repeat_no_enclosing_tags;
710
711             my $replaceby = join( "\n", @lines );
712             $letter->{content} =~ s/\Q$line\E/$replaceby/s;
713         }
714     }
715
716     $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
717 #   $letter->{content} =~ s/<<[^>]*>>//go;
718
719     return $letter;
720 }
721
722 sub _substitute_tables {
723     my ( $letter, $tables ) = @_;
724     while ( my ($table, $param) = each %$tables ) {
725         next unless $param;
726
727         my $ref = ref $param;
728
729         my $values;
730         if ($ref && $ref eq 'HASH') {
731             $values = $param;
732         }
733         else {
734             my $sth = _parseletter_sth($table);
735             unless ($sth) {
736                 warn "_parseletter_sth('$table') failed to return a valid sth.  No substitution will be done for that table.";
737                 return;
738             }
739             $sth->execute( $ref ? @$param : $param );
740
741             $values = $sth->fetchrow_hashref;
742             $sth->finish();
743         }
744
745         _parseletter ( $letter, $table, $values );
746     }
747 }
748
749 sub _parseletter_sth {
750     my $table = shift;
751     my $sth;
752     unless ($table) {
753         carp "ERROR: _parseletter_sth() called without argument (table)";
754         return;
755     }
756     # NOTE: we used to check whether we had a statement handle cached in
757     #       a %handles module-level variable. This was a dumb move and
758     #       broke things for the rest of us. prepare_cached is a better
759     #       way to cache statement handles anyway.
760     my $query = 
761     ($table eq 'biblio'       ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
762     ($table eq 'biblioitems'  ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
763     ($table eq 'items'        ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
764     ($table eq 'issues'       ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
765     ($table eq 'old_issues'   ) ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
766     ($table eq 'reserves'     ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
767     ($table eq 'borrowers'    ) ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
768     ($table eq 'branches'     ) ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
769     ($table eq 'suggestions'  ) ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
770     ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE             id = ?"                                  :
771     ($table eq 'aqorders'     ) ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
772     ($table eq 'opac_news'    ) ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
773     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
774     ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
775     ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
776     undef ;
777     unless ($query) {
778         warn "ERROR: No _parseletter_sth query for table '$table'";
779         return;     # nothing to get
780     }
781     unless ($sth = C4::Context->dbh->prepare_cached($query)) {
782         warn "ERROR: Failed to prepare query: '$query'";
783         return;
784     }
785     return $sth;    # now cache is populated for that $table
786 }
787
788 =head2 _parseletter($letter, $table, $values)
789
790     parameters :
791     - $letter : a hash to letter fields (title & content useful)
792     - $table : the Koha table to parse.
793     - $values : table record hashref
794     parse all fields from a table, and replace values in title & content with the appropriate value
795     (not exported sub, used only internally)
796
797 =cut
798
799 sub _parseletter {
800     my ( $letter, $table, $values ) = @_;
801
802     if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
803         $values->{'dateexpiry'} = format_sqldatetime( $values->{'dateexpiry'} );
804     }
805
806     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
807         my @waitingdate = split /-/, $values->{'waitingdate'};
808
809         $values->{'expirationdate'} = '';
810         if ( C4::Context->preference('ReservesMaxPickUpDelay') ) {
811             my $dt = dt_from_string();
812             $dt->add( days => C4::Context->preference('ReservesMaxPickUpDelay') );
813             $values->{'expirationdate'} = output_pref( { dt => $dt, dateonly => 1 } );
814         }
815
816         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
817
818     }
819
820     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
821         my $todaysdate = output_pref( DateTime->now() );
822         $letter->{content} =~ s/<<today>>/$todaysdate/go;
823     }
824
825     while ( my ($field, $val) = each %$values ) {
826         $val =~ s/\p{P}$// if $val && $table=~/biblio/;
827             #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
828             #Therefore adding the test on biblio. This includes biblioitems,
829             #but excludes items. Removed unneeded global and lookahead.
830
831         $val = GetAuthorisedValueByCode ('ROADTYPE', $val, 0) if $table=~/^borrowers$/ && $field=~/^streettype$/;
832
833         # Dates replacement
834         my $replacedby   = defined ($val) ? $val : '';
835         if (    $replacedby
836             and not $replacedby =~ m|0000-00-00|
837             and not $replacedby =~ m|9999-12-31|
838             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
839         {
840             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
841             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
842             my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
843
844             for my $letter_field ( qw( title content ) ) {
845                 my $filter_string_used = q{};
846                 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
847                     # We overwrite $dateonly if the filter exists and we have a time in the datetime
848                     $filter_string_used = $1 || q{};
849                     $dateonly = $1 unless $dateonly;
850                 }
851                 eval {
852                     $replacedby = output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
853                 };
854
855                 if ( $letter->{ $letter_field } ) {
856                     $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby/g;
857                     $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby/g;
858                 }
859             }
860         }
861         # Other fields replacement
862         else {
863             for my $letter_field ( qw( title content ) ) {
864                 if ( $letter->{ $letter_field } ) {
865                     $letter->{ $letter_field }   =~ s/<<$table.$field>>/$replacedby/g;
866                     $letter->{ $letter_field }   =~ s/<<$field>>/$replacedby/g;
867                 }
868             }
869         }
870     }
871
872     if ($table eq 'borrowers' && $letter->{content}) {
873         if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
874             my %attr;
875             foreach (@$attributes) {
876                 my $code = $_->{code};
877                 my $val  = $_->{value_description} || $_->{value};
878                 $val =~ s/\p{P}(?=$)//g if $val;
879                 next unless $val gt '';
880                 $attr{$code} ||= [];
881                 push @{ $attr{$code} }, $val;
882             }
883             while ( my ($code, $val_ar) = each %attr ) {
884                 my $replacefield = "<<borrower-attribute:$code>>";
885                 my $replacedby   = join ',', @$val_ar;
886                 $letter->{content} =~ s/$replacefield/$replacedby/g;
887             }
888         }
889     }
890     return $letter;
891 }
892
893 =head2 EnqueueLetter
894
895   my $success = EnqueueLetter( { letter => $letter, 
896         borrowernumber => '12', message_transport_type => 'email' } )
897
898 places a letter in the message_queue database table, which will
899 eventually get processed (sent) by the process_message_queue.pl
900 cronjob when it calls SendQueuedMessages.
901
902 return message_id on success
903
904 =cut
905
906 sub EnqueueLetter {
907     my $params = shift or return;
908
909     return unless exists $params->{'letter'};
910 #   return unless exists $params->{'borrowernumber'};
911     return unless exists $params->{'message_transport_type'};
912
913     my $content = $params->{letter}->{content};
914     $content =~ s/\s+//g if(defined $content);
915     if ( not defined $content or $content eq '' ) {
916         warn "Trying to add an empty message to the message queue" if $debug;
917         return;
918     }
919
920     # If we have any attachments we should encode then into the body.
921     if ( $params->{'attachments'} ) {
922         $params->{'letter'} = _add_attachments(
923             {   letter      => $params->{'letter'},
924                 attachments => $params->{'attachments'},
925                 message     => MIME::Lite->new( Type => 'multipart/mixed' ),
926             }
927         );
928     }
929
930     my $dbh       = C4::Context->dbh();
931     my $statement = << 'ENDSQL';
932 INSERT INTO message_queue
933 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
934 VALUES
935 ( ?,              ?,       ?,       ?,        ?,           ?,                      ?,      NOW(),       ?,          ?,            ? )
936 ENDSQL
937
938     my $sth    = $dbh->prepare($statement);
939     my $result = $sth->execute(
940         $params->{'borrowernumber'},              # borrowernumber
941         $params->{'letter'}->{'title'},           # subject
942         $params->{'letter'}->{'content'},         # content
943         $params->{'letter'}->{'metadata'} || '',  # metadata
944         $params->{'letter'}->{'code'}     || '',  # letter_code
945         $params->{'message_transport_type'},      # message_transport_type
946         'pending',                                # status
947         $params->{'to_address'},                  # to_address
948         $params->{'from_address'},                # from_address
949         $params->{'letter'}->{'content-type'},    # content_type
950     );
951     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
952 }
953
954 =head2 SendQueuedMessages ([$hashref]) 
955
956   my $sent = SendQueuedMessages( { verbose => 1 } );
957
958 sends all of the 'pending' items in the message queue.
959
960 returns number of messages sent.
961
962 =cut
963
964 sub SendQueuedMessages {
965     my $params = shift;
966
967     my $unsent_messages = _get_unsent_messages();
968     MESSAGE: foreach my $message ( @$unsent_messages ) {
969         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
970         warn sprintf( 'sending %s message to patron: %s',
971                       $message->{'message_transport_type'},
972                       $message->{'borrowernumber'} || 'Admin' )
973           if $params->{'verbose'} or $debug;
974         # This is just begging for subclassing
975         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
976         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
977             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
978         }
979         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
980             _send_message_by_sms( $message );
981         }
982     }
983     return scalar( @$unsent_messages );
984 }
985
986 =head2 GetRSSMessages
987
988   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
989
990 returns a listref of all queued RSS messages for a particular person.
991
992 =cut
993
994 sub GetRSSMessages {
995     my $params = shift;
996
997     return unless $params;
998     return unless ref $params;
999     return unless $params->{'borrowernumber'};
1000     
1001     return _get_unsent_messages( { message_transport_type => 'rss',
1002                                    limit                  => $params->{'limit'},
1003                                    borrowernumber         => $params->{'borrowernumber'}, } );
1004 }
1005
1006 =head2 GetPrintMessages
1007
1008   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1009
1010 Returns a arrayref of all queued print messages (optionally, for a particular
1011 person).
1012
1013 =cut
1014
1015 sub GetPrintMessages {
1016     my $params = shift || {};
1017     
1018     return _get_unsent_messages( { message_transport_type => 'print',
1019                                    borrowernumber         => $params->{'borrowernumber'},
1020                                  } );
1021 }
1022
1023 =head2 GetQueuedMessages ([$hashref])
1024
1025   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1026
1027 fetches messages out of the message queue.
1028
1029 returns:
1030 list of hashes, each has represents a message in the message queue.
1031
1032 =cut
1033
1034 sub GetQueuedMessages {
1035     my $params = shift;
1036
1037     my $dbh = C4::Context->dbh();
1038     my $statement = << 'ENDSQL';
1039 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1040 FROM message_queue
1041 ENDSQL
1042
1043     my @query_params;
1044     my @whereclauses;
1045     if ( exists $params->{'borrowernumber'} ) {
1046         push @whereclauses, ' borrowernumber = ? ';
1047         push @query_params, $params->{'borrowernumber'};
1048     }
1049
1050     if ( @whereclauses ) {
1051         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1052     }
1053
1054     if ( defined $params->{'limit'} ) {
1055         $statement .= ' LIMIT ? ';
1056         push @query_params, $params->{'limit'};
1057     }
1058
1059     my $sth = $dbh->prepare( $statement );
1060     my $result = $sth->execute( @query_params );
1061     return $sth->fetchall_arrayref({});
1062 }
1063
1064 =head2 GetMessageTransportTypes
1065
1066   my @mtt = GetMessageTransportTypes();
1067
1068   returns an arrayref of transport types
1069
1070 =cut
1071
1072 sub GetMessageTransportTypes {
1073     my $dbh = C4::Context->dbh();
1074     my $mtts = $dbh->selectcol_arrayref("
1075         SELECT message_transport_type
1076         FROM message_transport_types
1077         ORDER BY message_transport_type
1078     ");
1079     return $mtts;
1080 }
1081
1082 =head2 _add_attachements
1083
1084 named parameters:
1085 letter - the standard letter hashref
1086 attachments - listref of attachments. each attachment is a hashref of:
1087   type - the mime type, like 'text/plain'
1088   content - the actual attachment
1089   filename - the name of the attachment.
1090 message - a MIME::Lite object to attach these to.
1091
1092 returns your letter object, with the content updated.
1093
1094 =cut
1095
1096 sub _add_attachments {
1097     my $params = shift;
1098
1099     my $letter = $params->{'letter'};
1100     my $attachments = $params->{'attachments'};
1101     return $letter unless @$attachments;
1102     my $message = $params->{'message'};
1103
1104     # First, we have to put the body in as the first attachment
1105     $message->attach(
1106         Type => $letter->{'content-type'} || 'TEXT',
1107         Data => $letter->{'is_html'}
1108             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1109             : $letter->{'content'},
1110     );
1111
1112     foreach my $attachment ( @$attachments ) {
1113         $message->attach(
1114             Type     => $attachment->{'type'},
1115             Data     => $attachment->{'content'},
1116             Filename => $attachment->{'filename'},
1117         );
1118     }
1119     # we're forcing list context here to get the header, not the count back from grep.
1120     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1121     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1122     $letter->{'content'} = $message->body_as_string;
1123
1124     return $letter;
1125
1126 }
1127
1128 sub _get_unsent_messages {
1129     my $params = shift;
1130
1131     my $dbh = C4::Context->dbh();
1132     my $statement = << 'ENDSQL';
1133 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code
1134   FROM message_queue mq
1135   LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1136  WHERE status = ?
1137 ENDSQL
1138
1139     my @query_params = ('pending');
1140     if ( ref $params ) {
1141         if ( $params->{'message_transport_type'} ) {
1142             $statement .= ' AND message_transport_type = ? ';
1143             push @query_params, $params->{'message_transport_type'};
1144         }
1145         if ( $params->{'borrowernumber'} ) {
1146             $statement .= ' AND borrowernumber = ? ';
1147             push @query_params, $params->{'borrowernumber'};
1148         }
1149         if ( $params->{'limit'} ) {
1150             $statement .= ' limit ? ';
1151             push @query_params, $params->{'limit'};
1152         }
1153     }
1154
1155     $debug and warn "_get_unsent_messages SQL: $statement";
1156     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1157     my $sth = $dbh->prepare( $statement );
1158     my $result = $sth->execute( @query_params );
1159     return $sth->fetchall_arrayref({});
1160 }
1161
1162 sub _send_message_by_email {
1163     my $message = shift or return;
1164     my ($username, $password, $method) = @_;
1165
1166     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1167     my $to_address = $message->{'to_address'};
1168     unless ($to_address) {
1169         unless ($member) {
1170             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1171             _set_message_status( { message_id => $message->{'message_id'},
1172                                    status     => 'failed' } );
1173             return;
1174         }
1175         $to_address = C4::Members::GetNoticeEmailAddress( $message->{'borrowernumber'} );
1176         unless ($to_address) {  
1177             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1178             # warning too verbose for this more common case?
1179             _set_message_status( { message_id => $message->{'message_id'},
1180                                    status     => 'failed' } );
1181             return;
1182         }
1183     }
1184
1185     my $utf8   = decode('MIME-Header', $message->{'subject'} );
1186     $message->{subject}= encode('MIME-Header', $utf8);
1187     my $subject = encode('UTF-8', $message->{'subject'});
1188     my $content = encode('UTF-8', $message->{'content'});
1189     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1190     my $is_html = $content_type =~ m/html/io;
1191     my $branch_email = undef;
1192     my $branch_replyto = undef;
1193     my $branch_returnpath = undef;
1194     if ($member){
1195         my $branchdetail = GetBranchDetail( $member->{'branchcode'} );
1196         $branch_email = $branchdetail->{'branchemail'};
1197         $branch_replyto = $branchdetail->{'branchreplyto'};
1198         $branch_returnpath = $branchdetail->{'branchreturnpath'};
1199     }
1200     my $email = Koha::Email->new();
1201     my %sendmail_params = $email->create_message_headers(
1202         {
1203             to      => $to_address,
1204             from    => $message->{'from_address'} || $branch_email,
1205             replyto => $branch_replyto,
1206             sender  => $branch_returnpath,
1207             subject => $subject,
1208             message => $is_html ? _wrap_html( $content, $subject ) : $content,
1209             contenttype => $content_type
1210         }
1211     );
1212
1213     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1214     if ( my $bcc = C4::Context->preference('OverdueNoticeBcc') ) {
1215        $sendmail_params{ Bcc } = $bcc;
1216     }
1217
1218     _update_message_to_address($message->{'message_id'},$to_address) unless $message->{to_address}; #if initial message address was empty, coming here means that a to address was found and queue should be updated
1219     if ( sendmail( %sendmail_params ) ) {
1220         _set_message_status( { message_id => $message->{'message_id'},
1221                 status     => 'sent' } );
1222         return 1;
1223     } else {
1224         _set_message_status( { message_id => $message->{'message_id'},
1225                 status     => 'failed' } );
1226         carp $Mail::Sendmail::error;
1227         return;
1228     }
1229 }
1230
1231 sub _wrap_html {
1232     my ($content, $title) = @_;
1233
1234     my $css = C4::Context->preference("NoticeCSS") || '';
1235     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1236     return <<EOS;
1237 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1238     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1239 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1240 <head>
1241 <title>$title</title>
1242 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1243 $css
1244 </head>
1245 <body>
1246 $content
1247 </body>
1248 </html>
1249 EOS
1250 }
1251
1252 sub _is_duplicate {
1253     my ( $message ) = @_;
1254     my $dbh = C4::Context->dbh;
1255     my $count = $dbh->selectrow_array(q|
1256         SELECT COUNT(*)
1257         FROM message_queue
1258         WHERE message_transport_type = ?
1259         AND borrowernumber = ?
1260         AND letter_code = ?
1261         AND CAST(time_queued AS date) = CAST(NOW() AS date)
1262         AND status="sent"
1263         AND content = ?
1264     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1265     return $count;
1266 }
1267
1268 sub _send_message_by_sms {
1269     my $message = shift or return;
1270     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1271
1272     unless ( $member->{smsalertnumber} ) {
1273         _set_message_status( { message_id => $message->{'message_id'},
1274                                status     => 'failed' } );
1275         return;
1276     }
1277
1278     if ( _is_duplicate( $message ) ) {
1279         _set_message_status( { message_id => $message->{'message_id'},
1280                                status     => 'failed' } );
1281         return;
1282     }
1283
1284     my $success = C4::SMS->send_sms( { destination => $member->{'smsalertnumber'},
1285                                        message     => $message->{'content'},
1286                                      } );
1287     _set_message_status( { message_id => $message->{'message_id'},
1288                            status     => ($success ? 'sent' : 'failed') } );
1289     return $success;
1290 }
1291
1292 sub _update_message_to_address {
1293     my ($id, $to)= @_;
1294     my $dbh = C4::Context->dbh();
1295     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1296 }
1297
1298 sub _set_message_status {
1299     my $params = shift or return;
1300
1301     foreach my $required_parameter ( qw( message_id status ) ) {
1302         return unless exists $params->{ $required_parameter };
1303     }
1304
1305     my $dbh = C4::Context->dbh();
1306     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1307     my $sth = $dbh->prepare( $statement );
1308     my $result = $sth->execute( $params->{'status'},
1309                                 $params->{'message_id'} );
1310     return $result;
1311 }
1312
1313
1314 1;
1315 __END__