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