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