Bug 34731: Throw exception if SendQueuedMessages is passed a bad message_id
[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 Modern::Perl;
21
22 use Carp qw( carp croak );
23 use Template;
24 use Module::Load::Conditional qw( can_load );
25
26 use Try::Tiny;
27
28 use C4::Log qw( logaction );
29 use C4::Members;
30 use C4::SMS;
31 use C4::Templates;
32 use Koha::Auth::TwoFactorAuth;
33 use Koha::DateUtils qw( dt_from_string output_pref );
34 use Koha::Email;
35 use Koha::Exceptions;
36 use Koha::Notice::Messages;
37 use Koha::Notice::Templates;
38 use Koha::Notice::Util;
39 use Koha::Patrons;
40 use Koha::SMS::Providers;
41 use Koha::SMTP::Servers;
42 use Koha::Subscriptions;
43
44 use constant SERIALIZED_EMAIL_CONTENT_TYPE => 'message/rfc822';
45
46 our (@ISA, @EXPORT_OK);
47 BEGIN {
48     require Exporter;
49     @ISA = qw(Exporter);
50     @EXPORT_OK = qw(
51       GetLetters
52       GetLettersAvailableForALibrary
53       GetLetterTemplates
54       DelLetter
55       GetPreparedLetter
56       GetWrappedLetter
57       SendAlerts
58       GetPrintMessages
59       GetQueuedMessages
60       GetMessage
61       GetMessageTransportTypes
62
63       EnqueueLetter
64       SendQueuedMessages
65       ResendMessage
66     );
67 }
68
69 our $domain_limits = {};
70
71 =head1 NAME
72
73 C4::Letters - Give functions for Letters management
74
75 =head1 SYNOPSIS
76
77   use C4::Letters;
78
79 =head1 DESCRIPTION
80
81   "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
82   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)
83
84   Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
85
86 =head2 GetLetters([$module])
87
88   $letters = &GetLetters($module);
89   returns informations about letters.
90   if needed, $module filters for letters given module
91
92   DEPRECATED - You must use Koha::Notice::Templates instead
93   The group by clause is confusing and can lead to issues
94
95 =cut
96
97 sub GetLetters {
98     my ($filters) = @_;
99     my $module    = $filters->{module};
100     my $code      = $filters->{code};
101     my $branchcode = $filters->{branchcode};
102     my $dbh       = C4::Context->dbh;
103     my $letters   = $dbh->selectall_arrayref(
104         q|
105             SELECT code, module, name
106             FROM letter
107             WHERE 1
108         |
109           . ( $module ? q| AND module = ?| : q|| )
110           . ( $code   ? q| AND code = ?|   : q|| )
111           . ( defined $branchcode   ? q| AND branchcode = ?|   : q|| )
112           . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
113         , ( $module ? $module : () )
114         , ( $code ? $code : () )
115         , ( defined $branchcode ? $branchcode : () )
116     );
117
118     return $letters;
119 }
120
121 =head2 GetLetterTemplates
122
123     my $letter_templates = GetLetterTemplates(
124         {
125             module => 'circulation',
126             code => 'my code',
127             branchcode => 'CPL', # '' for default,
128         }
129     );
130
131     Return a hashref of letter templates.
132
133 =cut
134
135 sub GetLetterTemplates {
136     my ( $params ) = @_;
137
138     my $module    = $params->{module};
139     my $code      = $params->{code};
140     my $branchcode = $params->{branchcode} // '';
141     my $dbh       = C4::Context->dbh;
142     return Koha::Notice::Templates->search(
143         {
144             module     => $module,
145             code       => $code,
146             branchcode => $branchcode,
147             (
148                 C4::Context->preference('TranslateNotices')
149                 ? ()
150                 : ( lang => 'default' )
151             )
152         }
153     )->unblessed;
154 }
155
156 =head2 GetLettersAvailableForALibrary
157
158     my $letters = GetLettersAvailableForALibrary(
159         {
160             branchcode => 'CPL', # '' for default
161             module => 'circulation',
162         }
163     );
164
165     Return an arrayref of letters, sorted by name.
166     If a specific letter exist for the given branchcode, it will be retrieve.
167     Otherwise the default letter will be.
168
169 =cut
170
171 sub GetLettersAvailableForALibrary {
172     my ($filters)  = @_;
173     my $branchcode = $filters->{branchcode};
174     my $module     = $filters->{module};
175
176     croak "module should be provided" unless $module;
177
178     my $dbh             = C4::Context->dbh;
179     my $default_letters = $dbh->selectall_arrayref(
180         q|
181             SELECT module, code, branchcode, name
182             FROM letter
183             WHERE 1
184         |
185           . q| AND branchcode = ''|
186           . ( $module ? q| AND module = ?| : q|| )
187           . q| ORDER BY name|, { Slice => {} }
188         , ( $module ? $module : () )
189     );
190
191     my $specific_letters;
192     if ($branchcode) {
193         $specific_letters = $dbh->selectall_arrayref(
194             q|
195                 SELECT module, code, branchcode, name
196                 FROM letter
197                 WHERE 1
198             |
199               . q| AND branchcode = ?|
200               . ( $module ? q| AND module = ?| : q|| )
201               . q| ORDER BY name|, { Slice => {} }
202             , $branchcode
203             , ( $module ? $module : () )
204         );
205     }
206
207     my %letters;
208     for my $l (@$default_letters) {
209         $letters{ $l->{code} } = $l;
210     }
211     for my $l (@$specific_letters) {
212         # Overwrite the default letter with the specific one.
213         $letters{ $l->{code} } = $l;
214     }
215
216     return [ map { $letters{$_} }
217           sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
218           keys %letters ];
219
220 }
221
222 =head2 DelLetter
223
224     DelLetter(
225         {
226             branchcode => 'CPL',
227             module => 'circulation',
228             code => 'my code',
229             [ mtt => 'email', ]
230         }
231     );
232
233     Delete the letter. The mtt parameter is facultative.
234     If not given, all templates mathing the other parameters will be removed.
235
236 =cut
237
238 sub DelLetter {
239     my ($params)   = @_;
240     my $branchcode = $params->{branchcode};
241     my $module     = $params->{module};
242     my $code       = $params->{code};
243     my $mtt        = $params->{mtt};
244     my $lang       = $params->{lang};
245     my $dbh        = C4::Context->dbh;
246     $dbh->do(q|
247         DELETE FROM letter
248         WHERE branchcode = ?
249           AND module = ?
250           AND code = ?
251     |
252     . ( $mtt ? q| AND message_transport_type = ?| : q|| )
253     . ( $lang? q| AND lang = ?| : q|| )
254     , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
255 }
256
257 =head2 SendAlerts
258
259     my $err = &SendAlerts($type, $externalid, $letter_code);
260
261     Parameters:
262       - $type : the type of alert
263       - $externalid : the id of the "object" to query
264       - $letter_code : the notice template to use
265
266     C<&SendAlerts> sends an email notice directly to a patron or a vendor.
267
268     Currently it supports ($type):
269       - claim serial issues (claimissues)
270       - claim acquisition orders (claimacquisition)
271       - send acquisition orders to the vendor (orderacquisition)
272       - notify patrons about newly received serial issues (issue)
273       - notify patrons when their account is created (members)
274
275     Returns undef or { error => 'message } on failure.
276     Returns true on success.
277
278 =cut
279
280 sub SendAlerts {
281     my ( $type, $externalid, $letter_code ) = @_;
282     my $dbh = C4::Context->dbh;
283     my $error;
284
285     if ( $type eq 'issue' ) {
286
287         # prepare the letter...
288         # search the subscriptionid
289         my $sth =
290           $dbh->prepare(
291             "SELECT subscriptionid FROM serial WHERE serialid=?");
292         $sth->execute($externalid);
293         my ($subscriptionid) = $sth->fetchrow
294           or warn( "No subscription for '$externalid'" ),
295              return;
296
297         # search the biblionumber
298         $sth =
299           $dbh->prepare(
300             "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
301         $sth->execute($subscriptionid);
302         my ($biblionumber) = $sth->fetchrow
303           or warn( "No biblionumber for '$subscriptionid'" ),
304              return;
305
306         # find the list of subscribers to notify
307         my $subscription = Koha::Subscriptions->find( $subscriptionid );
308         my $subscribers = $subscription->subscribers;
309         while ( my $patron = $subscribers->next ) {
310             my $email = $patron->email or next;
311
312 #                    warn "sending issues...";
313             my $userenv = C4::Context->userenv;
314             my $library = $patron->library;
315             my $letter = GetPreparedLetter (
316                 module => 'serial',
317                 letter_code => $letter_code,
318                 branchcode => $userenv->{branch},
319                 tables => {
320                     'branches'    => $library->branchcode,
321                     'biblio'      => $biblionumber,
322                     'biblioitems' => $biblionumber,
323                     'borrowers'   => $patron->unblessed,
324                     'subscription' => $subscriptionid,
325                     'serial' => $externalid,
326                 },
327                 want_librarian => 1,
328             ) or return;
329
330             # FIXME: This 'default' behaviour should be moved to Koha::Email
331             my $mail = Koha::Email->create(
332                 {
333                     to       => $email,
334                     from     => $library->branchemail,
335                     reply_to => $library->branchreplyto,
336                     sender   => $library->branchreturnpath,
337                     subject  => "" . $letter->{title},
338                 }
339             );
340
341             if ( $letter->{is_html} ) {
342                 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
343             }
344             else {
345                 $mail->text_body( $letter->{content} );
346             }
347
348             my $success = try {
349                 $mail->send_or_die({ transport => $library->smtp_server->transport });
350             }
351             catch {
352                 # We expect ref($_) eq 'Email::Sender::Failure'
353                 $error = $_->message;
354
355                 carp "$_";
356                 return;
357             };
358
359             return { error => $error }
360                 unless $success;
361         }
362     }
363     elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
364
365         # prepare the letter...
366         my $strsth;
367         my $sthorders;
368         my $dataorders;
369         my $action;
370         my $basketno;
371         my %loops;
372         if ( $type eq 'claimacquisition') {
373             $strsth = qq{
374             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
375             FROM aqorders
376             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
377             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
378             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
379             WHERE aqorders.ordernumber IN (
380             };
381
382             if (!@$externalid){
383                 carp "No order selected";
384                 return { error => "no_order_selected" };
385             }
386             $strsth .= join( ",", ('?') x @$externalid ) . ")";
387             $action = "ACQUISITION CLAIM";
388             $sthorders = $dbh->prepare($strsth);
389             $sthorders->execute( @$externalid );
390             $dataorders = $sthorders->fetchall_arrayref( {} );
391         }
392
393         if ($type eq 'claimissues') {
394             $strsth = qq{
395             SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
396             aqbooksellers.id AS booksellerid
397             FROM serial
398             LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
399             LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
400             LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
401             LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
402             WHERE serial.serialid IN (
403             };
404
405             if (!@$externalid){
406                 carp "No issues selected";
407                 return { error => "no_issues_selected" };
408             }
409
410             $strsth .= join( ",", ('?') x @$externalid ) . ")";
411             $action = "SERIAL CLAIM";
412             $sthorders = $dbh->prepare($strsth);
413             $sthorders->execute( @$externalid );
414             $dataorders = $sthorders->fetchall_arrayref( {} );
415         }
416
417         if ( $type eq 'orderacquisition') {
418             $basketno = $externalid;
419             $strsth = qq{
420             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
421             FROM aqorders
422             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
423             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
424             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
425             WHERE aqbasket.basketno = ?
426             AND orderstatus IN ('new','ordered')
427             };
428
429             unless ( $basketno ) {
430                 carp "No basketnumber given";
431                 return { error => "no_basketno" };
432             }
433             $action = "ACQUISITION ORDER";
434             $sthorders = $dbh->prepare($strsth);
435             $sthorders->execute($basketno);
436             $dataorders = $sthorders->fetchall_arrayref( {} );
437             %loops = (
438                 aqorders => [ map { $_->{ordernumber} } @$dataorders ]
439             );
440         }
441
442         my $booksellerid = $dataorders->[0]->{booksellerid};
443         my $addressee =  $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
444
445         my $sthcontact =
446           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
447         $sthcontact->execute( $booksellerid );
448         my $datacontact = $sthcontact->fetchrow_hashref;
449
450         my @email;
451         my @cc;
452         push @email, $datacontact->{email}           if ( $datacontact && $datacontact->{email} );
453         unless (@email) {
454             warn "Bookseller $booksellerid without emails";
455             return { error => "no_email" };
456         }
457         my $addlcontact;
458         while ($addlcontact = $sthcontact->fetchrow_hashref) {
459             push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
460         }
461
462         my $userenv = C4::Context->userenv;
463         my $letter = GetPreparedLetter (
464             module => $type,
465             letter_code => $letter_code,
466             branchcode => $userenv->{branch},
467             tables => {
468                 'branches'      => $userenv->{branch},
469                 'aqbooksellers' => $booksellerid,
470                 'aqcontacts'    => $datacontact,
471                 'aqbasket'      => $basketno,
472             },
473             repeat => $dataorders,
474             loops => \%loops,
475             want_librarian => 1,
476         ) or return { error => "no_letter" };
477
478         # Remove the order tag
479         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
480
481         # ... then send mail
482         my $library = Koha::Libraries->find( $userenv->{branch} );
483         my $mail = Koha::Email->create(
484             {
485                 to => join( ',', @email ),
486                 cc => join( ',', @cc ),
487                 (
488                     (
489                         C4::Context->preference("ClaimsBccCopy")
490                           && ( $type eq 'claimacquisition'
491                             || $type eq 'claimissues' )
492                     )
493                     ? ( bcc => $userenv->{emailaddress} )
494                     : ()
495                 ),
496                 from => $library->branchemail
497                   || C4::Context->preference('KohaAdminEmailAddress'),
498                 subject => "" . $letter->{title},
499             }
500         );
501
502         if ( $letter->{is_html} ) {
503             $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
504         }
505         else {
506             $mail->text_body( "" . $letter->{content} );
507         }
508
509         my $success = try {
510             $mail->send_or_die({ transport => $library->smtp_server->transport });
511         }
512         catch {
513             # We expect ref($_) eq 'Email::Sender::Failure'
514             $error = $_->message;
515
516             carp "$_";
517             return;
518         };
519
520         return { error => $error }
521             unless $success;
522
523         my $log_object = $action eq 'ACQUISITION ORDER' ? $externalid : undef;
524         my $module = $action eq 'ACQUISITION ORDER' ? 'ACQUISITIONS' : 'CLAIMS';
525         logaction(
526             $module,
527             $action,
528             $log_object,
529             "To="
530                 . join( ',', @email )
531                 . " Title="
532                 . $letter->{title}
533                 . " Content="
534                 . $letter->{content}
535         ) if C4::Context->preference("ClaimsLog");
536     }
537
538     # If we come here, return an OK status
539     return 1;
540 }
541
542 =head2 GetPreparedLetter( %params )
543
544     %params hash:
545       module => letter module, mandatory
546       letter_code => letter code, mandatory
547       branchcode => for letter selection, if missing default system letter taken
548       tables => a hashref with table names as keys. Values are either:
549         - a scalar - primary key value
550         - an arrayref - primary key values
551         - a hashref - full record
552       substitute => custom substitution key/value pairs
553       repeat => records to be substituted on consecutive lines:
554         - an arrayref - tries to guess what needs substituting by
555           taking remaining << >> tokensr; not recommended
556         - a hashref token => @tables - replaces <token> << >> << >> </token>
557           subtemplate for each @tables row; table is a hashref as above
558       want_librarian => boolean,  if set to true triggers librarian details
559         substitution from the userenv
560     Return value:
561       letter fields hashref (title & content useful)
562
563 =cut
564
565 sub GetPreparedLetter {
566     my %params = @_;
567
568     my $letter = $params{letter};
569     my $lang   = $params{lang} || 'default';
570
571     unless ( $letter ) {
572         my $module      = $params{module} or croak "No module";
573         my $letter_code = $params{letter_code} or croak "No letter_code";
574         my $branchcode  = $params{branchcode} || '';
575         my $mtt         = $params{message_transport_type} || 'email';
576
577         my $template = Koha::Notice::Templates->find_effective_template(
578             {
579                 module                 => $module,
580                 code                   => $letter_code,
581                 branchcode             => $branchcode,
582                 message_transport_type => $mtt,
583                 lang                   => $lang
584             }
585         );
586
587         unless ( $template ) {
588             warn( "No $module $letter_code letter transported by " . $mtt );
589             return;
590         }
591
592         $letter = $template->unblessed;
593         $letter->{'content-type'} = 'text/html; charset="UTF-8"' if $letter->{is_html};
594     }
595
596     my $objects = $params{objects} || {};
597     my $tables = $params{tables} || {};
598     my $substitute = $params{substitute} || {};
599     my $loops  = $params{loops} || {}; # loops is not supported for historical notices syntax
600     my $repeat = $params{repeat};
601     %$tables || %$substitute || $repeat || %$loops || %$objects
602       or carp( "ERROR: nothing to substitute - all of 'objects', 'tables', 'loops' and 'substitute' are empty" ),
603          return;
604     my $want_librarian = $params{want_librarian};
605
606     if (%$substitute) {
607         while ( my ($token, $val) = each %$substitute ) {
608             $val //= q{};
609             if ( $token eq 'items.content' ) {
610                 $val =~ s|\n|<br/>|g if $letter->{is_html};
611             }
612
613             $letter->{title} =~ s/<<$token>>/$val/g;
614             $letter->{content} =~ s/<<$token>>/$val/g;
615        }
616     }
617
618     my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
619     $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
620
621     if ($want_librarian) {
622         # parsing librarian name
623         my $userenv = C4::Context->userenv;
624         $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
625         $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
626         $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
627     }
628
629     my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
630
631     if ($repeat) {
632         if (ref ($repeat) eq 'ARRAY' ) {
633             $repeat_no_enclosing_tags = $repeat;
634         } else {
635             $repeat_enclosing_tags = $repeat;
636         }
637     }
638
639     if ($repeat_enclosing_tags) {
640         while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
641             if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
642                 my $subcontent = $1;
643                 my @lines = map {
644                     my %subletter = ( title => '', content => $subcontent );
645                     _substitute_tables( \%subletter, $_ );
646                     $subletter{content};
647                 } @$tag_tables;
648                 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
649             }
650         }
651     }
652
653     if (%$tables) {
654         _substitute_tables( $letter, $tables );
655     }
656
657     if ($repeat_no_enclosing_tags) {
658         if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
659             my $line = $&;
660             my $i = 1;
661             my @lines = map {
662                 my $c = $line;
663                 $c =~ s/<<count>>/$i/go;
664                 foreach my $field ( keys %{$_} ) {
665                     $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
666                 }
667                 $i++;
668                 $c;
669             } @$repeat_no_enclosing_tags;
670
671             my $replaceby = join( "\n", @lines );
672             $letter->{content} =~ s/\Q$line\E/$replaceby/s;
673         }
674     }
675
676     $letter->{content} = _process_tt(
677         {
678             content    => $letter->{content},
679             lang       => $lang,
680             loops      => $loops,
681             objects    => $objects,
682             substitute => $substitute,
683             tables     => $tables,
684         }
685     );
686
687     $letter->{title} = _process_tt(
688         {
689             content    => $letter->{title},
690             lang       => $lang,
691             loops      => $loops,
692             objects    => $objects,
693             substitute => $substitute,
694             tables     => $tables,
695         }
696     );
697
698     $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
699
700     return $letter;
701 }
702
703 sub _substitute_tables {
704     my ( $letter, $tables ) = @_;
705     while ( my ($table, $param) = each %$tables ) {
706         next unless $param;
707
708         my $ref = ref $param;
709
710         my $values;
711         if ($ref && $ref eq 'HASH') {
712             $values = $param;
713         }
714         else {
715             my $sth = _parseletter_sth($table);
716             unless ($sth) {
717                 warn "_parseletter_sth('$table') failed to return a valid sth.  No substitution will be done for that table.";
718                 return;
719             }
720             $sth->execute( $ref ? @$param : $param );
721
722             $values = $sth->fetchrow_hashref;
723             $sth->finish();
724         }
725
726         _parseletter ( $letter, $table, $values );
727     }
728 }
729
730 sub _parseletter_sth {
731     my $table = shift;
732     my $sth;
733     unless ($table) {
734         carp "ERROR: _parseletter_sth() called without argument (table)";
735         return;
736     }
737     # NOTE: we used to check whether we had a statement handle cached in
738     #       a %handles module-level variable. This was a dumb move and
739     #       broke things for the rest of us. prepare_cached is a better
740     #       way to cache statement handles anyway.
741     my $query = 
742     ($table eq 'accountlines' )    ? "SELECT * FROM $table WHERE   accountlines_id = ?"                               :
743     ($table eq 'biblio'       )    ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
744     ($table eq 'biblioitems'  )    ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
745     ($table eq 'tickets'      )    ? "SELECT * FROM $table WHERE   id = ?"                                            :
746     ($table eq 'ticket_updates' )  ? "SELECT * FROM $table WHERE   id = ?"                                            :
747     ($table eq 'credits'      )    ? "SELECT * FROM accountlines WHERE   accountlines_id = ?"                         :
748     ($table eq 'debits'       )    ? "SELECT * FROM accountlines WHERE   accountlines_id = ?"                         :
749     ($table eq 'items'        )    ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
750     ($table eq 'issues'       )    ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
751     ($table eq 'old_issues'   )    ? "SELECT * FROM $table WHERE     issue_id = ?"  :
752     ($table eq 'reserves'     )    ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
753     ($table eq 'borrowers'    )    ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
754     ($table eq 'branches'     )    ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
755     ($table eq 'suggestions'  )    ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
756     ($table eq 'aqbooksellers')    ? "SELECT * FROM $table WHERE             id = ?"                                  :
757     ($table eq 'aqorders'     )    ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
758     ($table eq 'aqbasket'     )    ? "SELECT * FROM $table WHERE       basketno = ?"                                  :
759     ($table eq 'illrequests'  )    ? "SELECT * FROM $table WHERE  illrequest_id = ?"                                  :
760     ($table eq 'article_requests') ? "SELECT * FROM $table WHERE             id = ?"                                  :
761     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
762     ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
763     ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
764     ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
765     ($table eq 'additional_contents' || $table eq 'opac_news') ? "SELECT * FROM additional_contents WHERE idnew = ?" :
766     ($table eq 'recalls') ? "SELECT * FROM $table WHERE recall_id = ?" :
767     undef ;
768     unless ($query) {
769         warn "ERROR: No _parseletter_sth query for table '$table'";
770         return;     # nothing to get
771     }
772     unless ($sth = C4::Context->dbh->prepare_cached($query)) {
773         warn "ERROR: Failed to prepare query: '$query'";
774         return;
775     }
776     return $sth;    # now cache is populated for that $table
777 }
778
779 =head2 _parseletter($letter, $table, $values)
780
781     parameters :
782     - $letter : a hash to letter fields (title & content useful)
783     - $table : the Koha table to parse.
784     - $values_in : table record hashref
785     parse all fields from a table, and replace values in title & content with the appropriate value
786     (not exported sub, used only internally)
787
788 =cut
789
790 sub _parseletter {
791     my ( $letter, $table, $values_in ) = @_;
792
793     # Work on a local copy of $values_in (passed by reference) to avoid side effects
794     # in callers ( by changing / formatting values )
795     my $values = $values_in ? { %$values_in } : {};
796
797     # FIXME Dates formatting must be done in notice's templates
798     if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
799         $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
800     }
801
802     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
803         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
804     }
805
806     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
807         my $todaysdate = output_pref( dt_from_string() );
808         $letter->{content} =~ s/<<today>>/$todaysdate/go;
809     }
810
811     while ( my ($field, $val) = each %$values ) {
812         $val =~ s/\p{P}$// if $val && $table=~/biblio/;
813             #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
814             #Therefore adding the test on biblio. This includes biblioitems,
815             #but excludes items. Removed unneeded global and lookahead.
816
817         if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
818             my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
819             $val = $av->count ? $av->next->lib : '';
820         }
821
822         # Dates replacement
823         my $replacedby   = defined ($val) ? $val : '';
824         if (    $replacedby
825             and not $replacedby =~ m|9999-12-31|
826             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
827         {
828             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
829             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
830             my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
831
832             for my $letter_field ( qw( title content ) ) {
833                 my $filter_string_used = q{};
834                 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
835                     # We overwrite $dateonly if the filter exists and we have a time in the datetime
836                     $filter_string_used = $1 || q{};
837                     $dateonly = $1 unless $dateonly;
838                 }
839                 my $replacedby_date = eval {
840                     output_pref({ dt => scalar dt_from_string( $replacedby ), dateonly => $dateonly });
841                 };
842                 $replacedby_date //= q{};
843
844                 if ( $letter->{ $letter_field } ) {
845                     $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
846                     $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
847                 }
848             }
849         }
850         # Other fields replacement
851         else {
852             for my $letter_field ( qw( title content ) ) {
853                 if ( $letter->{ $letter_field } ) {
854                     $letter->{ $letter_field }   =~ s/<<$table.$field>>/$replacedby/g;
855                     $letter->{ $letter_field }   =~ s/<<$field>>/$replacedby/g;
856                 }
857             }
858         }
859     }
860
861     if ($table eq 'borrowers' && $letter->{content}) {
862         my $patron = Koha::Patrons->find( $values->{borrowernumber} );
863         if ( $patron ) {
864             my $attributes = $patron->extended_attributes;
865             my %attr;
866             while ( my $attribute = $attributes->next ) {
867                 my $code = $attribute->code;
868                 my $val  = $attribute->description; # FIXME - we always display intranet description here!
869                 $val =~ s/\p{P}(?=$)//g if $val;
870                 next unless $val gt '';
871                 $attr{$code} ||= [];
872                 push @{ $attr{$code} }, $val;
873             }
874             while ( my ($code, $val_ar) = each %attr ) {
875                 my $replacefield = "<<borrower-attribute:$code>>";
876                 my $replacedby   = join ',', @$val_ar;
877                 $letter->{content} =~ s/$replacefield/$replacedby/g;
878             }
879         }
880     }
881     return $letter;
882 }
883
884 =head2 EnqueueLetter
885
886   my $success = EnqueueLetter( { letter => $letter, 
887         borrowernumber => '12', message_transport_type => 'email' } )
888
889 Places a letter in the message_queue database table, which will
890 eventually get processed (sent) by the process_message_queue.pl
891 cronjob when it calls SendQueuedMessages.
892
893 Return message_id on success
894
895 Parameters
896 * letter - required; A letter hashref as returned from GetPreparedLetter
897 * message_transport_type - required; One of the available mtts
898 * borrowernumber - optional if 'to_address' is passed; The borrowernumber of the patron we enqueuing the notice for
899 * to_address - optional if 'borrowernumber' is passed; The destination email address for the notice (defaults to patron->notice_email_address)
900 * from_address - optional; The from address for the notice, defaults to patron->library->from_email_address
901 * reply_address - optional; The reply address for the notice, defaults to patron->library->reply_to
902
903 =cut
904
905 sub EnqueueLetter {
906     my $params = shift or return;
907
908     return unless exists $params->{'letter'};
909 #   return unless exists $params->{'borrowernumber'};
910     return unless exists $params->{'message_transport_type'};
911
912     my $content = $params->{letter}->{content};
913     $content =~ s/\s+//g if(defined $content);
914     if ( not defined $content or $content eq '' ) {
915         Koha::Logger->get->info("Trying to add an empty message to the message queue");
916         return;
917     }
918
919     # If we have any attachments we should encode then into the body.
920     if ( $params->{'attachments'} ) {
921         $params->{'letter'} = _add_attachments(
922             {   letter      => $params->{'letter'},
923                 attachments => $params->{'attachments'},
924             }
925         );
926     }
927
928     my $dbh       = C4::Context->dbh();
929     my $statement = << 'ENDSQL';
930 INSERT INTO message_queue
931 ( letter_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type, failure_code )
932 VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, CAST(NOW() AS DATETIME), ?, ?, ?, ?, ? )
933 ENDSQL
934
935     my $sth    = $dbh->prepare($statement);
936     my $result = $sth->execute(
937         $params->{letter}->{id} || undef,         # letter.id
938         $params->{'borrowernumber'},              # borrowernumber
939         $params->{'letter'}->{'title'},           # subject
940         $params->{'letter'}->{'content'},         # content
941         $params->{'letter'}->{'metadata'} || '',  # metadata
942         $params->{'letter'}->{'code'}     || '',  # letter_code
943         $params->{'message_transport_type'},      # message_transport_type
944         'pending',                                # status
945         $params->{'to_address'},                  # to_address
946         $params->{'from_address'},                # from_address
947         $params->{'reply_address'},               # reply_address
948         $params->{'letter'}->{'content-type'},    # content_type
949         $params->{'failure_code'}        || '',   # failure_code
950     );
951     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
952 }
953
954 =head2 SendQueuedMessages ([$hashref]) 
955
956     my $sent = SendQueuedMessages({
957         message_id => $id,
958         borrowernumber => $who_letter_is_for,
959         letter_code => $letter_code, # can be scalar or arrayref
960         type => $type, # can be scalar or arrayref
961         limit => 50,
962         verbose => 1,
963         where => $where,
964     });
965
966 Sends 'pending' messages from the queue, based on parameters.
967
968 The (optional) message_id, borrowernumber, letter_code, type and where
969 parameter are used to select which pending messages will be processed. The
970 limit parameter determines the volume of results, i.e. sent messages.
971
972 The optional verbose parameter can be used to generate debugging output.
973
974 Returns number of messages sent.
975
976 =cut
977
978 sub SendQueuedMessages {
979     my $params = shift;
980     my $limit = $params->{limit};
981     my $where = $params->{where};
982
983     Koha::Exceptions::BadParameter->throw("Parameter message_id cannot be empty if passed.")
984         if ( exists( $params->{message_id} ) && !$params->{message_id} );
985
986     my $smtp_transports = {};
987
988     my $count_messages = 0;
989     my $unsent_messages = Koha::Notice::Messages->search({
990         status  => 'pending',
991         $params->{message_id} ? ( message_id => $params->{message_id} ) : (),
992         $params->{borrowernumber} ? ( borrowernumber => $params->{borrowernumber} ) : (),
993         # Check for scalar or array in letter_code and type
994         ref($params->{letter_code}) && @{$params->{letter_code}} ? ( letter_code => $params->{letter_code} ) : (),
995         !ref($params->{letter_code}) && $params->{letter_code} ? ( letter_code => $params->{letter_code} ) : (),
996         ref($params->{type}) && @{$params->{type}} ? ( message_transport_type => $params->{type} ) : (), #TODO Existing inconsistency
997         !ref($params->{type}) && $params->{type} ? ( message_transport_type => $params->{type} ) : (), #TODO Existing inconsistency
998     });
999     $unsent_messages = $unsent_messages->search( \$where ) if $where;
1000
1001     $domain_limits = Koha::Notice::Util->load_domain_limits; # (re)initialize per run
1002     while( ( my $message_object = $unsent_messages->next ) && ( !$limit || $count_messages < $limit ) ) {
1003         my $message = $message_object->unblessed;
1004
1005         # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1006         $message_object->make_column_dirty('status');
1007         return unless $message_object->store;
1008
1009         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1010         warn sprintf( 'Processing %s message to patron: %s',
1011                       $message->{'message_transport_type'},
1012                       $message->{'borrowernumber'} || 'Admin' )
1013           if $params->{'verbose'};
1014         # This is just begging for subclassing
1015         next if ( lc($message->{'message_transport_type'}) eq 'rss' );
1016         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1017             my $rv = _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'}, $smtp_transports );
1018             $count_messages++ if $rv;
1019         }
1020         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1021             if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1022                 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1023                 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1024                 unless ( $sms_provider ) {
1025                     warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1026                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1027                     next;
1028                 }
1029                 unless ( $patron->smsalertnumber ) {
1030                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1031                     warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'};
1032                     next;
1033                 }
1034                 $message->{to_address}  = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1035                 $message->{to_address} .= '@' . $sms_provider->domain();
1036
1037                 # Check for possible from_address override
1038                 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1039                 if ($from_address && $message->{from_address} ne $from_address) {
1040                     $message->{from_address} = $from_address;
1041                     _update_message_from_address($message->{'message_id'}, $message->{from_address});
1042                 }
1043
1044                 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1045                 my $rv = _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1046                 $count_messages++ if $rv;
1047             } else {
1048                 my $rv = _send_message_by_sms( $message );
1049                 $count_messages++ if $rv;
1050             }
1051         }
1052     }
1053     return $count_messages;
1054 }
1055
1056 =head2 GetRSSMessages
1057
1058   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1059
1060 returns a listref of all queued RSS messages for a particular person.
1061
1062 =cut
1063
1064 sub GetRSSMessages {
1065     my $params = shift;
1066
1067     return unless $params;
1068     return unless ref $params;
1069     return unless $params->{'borrowernumber'};
1070     
1071     return _get_unsent_messages( { message_transport_type => 'rss',
1072                                    limit                  => $params->{'limit'},
1073                                    borrowernumber         => $params->{'borrowernumber'}, } );
1074 }
1075
1076 =head2 GetPrintMessages
1077
1078   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1079
1080 Returns a arrayref of all queued print messages (optionally, for a particular
1081 person).
1082
1083 =cut
1084
1085 sub GetPrintMessages {
1086     my $params = shift || {};
1087     
1088     return _get_unsent_messages( { message_transport_type => 'print',
1089                                    borrowernumber         => $params->{'borrowernumber'},
1090                                  } );
1091 }
1092
1093 =head2 GetQueuedMessages ([$hashref])
1094
1095   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1096
1097 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1098 and limited to specified limit.
1099
1100 Return is an arrayref of hashes, each has represents a message in the message queue.
1101
1102 =cut
1103
1104 sub GetQueuedMessages {
1105     my $params = shift;
1106
1107     my $dbh = C4::Context->dbh();
1108     my $statement = << 'ENDSQL';
1109 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on, failure_code
1110 FROM message_queue
1111 ENDSQL
1112
1113     my @query_params;
1114     my @whereclauses;
1115     if ( exists $params->{'borrowernumber'} ) {
1116         push @whereclauses, ' borrowernumber = ? ';
1117         push @query_params, $params->{'borrowernumber'};
1118     }
1119
1120     if ( @whereclauses ) {
1121         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1122     }
1123
1124     if ( defined $params->{'limit'} ) {
1125         $statement .= ' LIMIT ? ';
1126         push @query_params, $params->{'limit'};
1127     }
1128
1129     my $sth = $dbh->prepare( $statement );
1130     my $result = $sth->execute( @query_params );
1131     return $sth->fetchall_arrayref({});
1132 }
1133
1134 =head2 GetMessageTransportTypes
1135
1136   my @mtt = GetMessageTransportTypes();
1137
1138   returns an arrayref of transport types
1139
1140 =cut
1141
1142 sub GetMessageTransportTypes {
1143     my $dbh = C4::Context->dbh();
1144     my $mtts = $dbh->selectcol_arrayref("
1145         SELECT message_transport_type
1146         FROM message_transport_types
1147         ORDER BY message_transport_type
1148     ");
1149     return $mtts;
1150 }
1151
1152 =head2 GetMessage
1153
1154     my $message = C4::Letters::Message($message_id);
1155
1156 =cut
1157
1158 sub GetMessage {
1159     my ( $message_id ) = @_;
1160     return unless $message_id;
1161     my $dbh = C4::Context->dbh;
1162     return $dbh->selectrow_hashref(q|
1163         SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, updated_on, to_address, from_address, reply_address, content_type, failure_code
1164         FROM message_queue
1165         WHERE message_id = ?
1166     |, {}, $message_id );
1167 }
1168
1169 =head2 ResendMessage
1170
1171   Attempt to resend a message which has failed previously.
1172
1173   my $has_been_resent = C4::Letters::ResendMessage($message_id);
1174
1175   Updates the message to 'pending' status so that
1176   it will be resent later on.
1177
1178   returns 1 on success, 0 on failure, undef if no message was found
1179
1180 =cut
1181
1182 sub ResendMessage {
1183     my $message_id = shift;
1184     return unless $message_id;
1185
1186     my $message = GetMessage( $message_id );
1187     return unless $message;
1188     my $rv = 0;
1189     if ( $message->{status} ne 'pending' ) {
1190         $rv = C4::Letters::_set_message_status({
1191             message_id => $message_id,
1192             status => 'pending',
1193         });
1194         $rv = $rv > 0? 1: 0;
1195         # Clear destination email address to force address update
1196         _update_message_to_address( $message_id, undef ) if $rv &&
1197             $message->{message_transport_type} eq 'email';
1198     }
1199     return $rv;
1200 }
1201
1202 =head2 _add_attachements
1203
1204   _add_attachments({ letter => $letter, attachments => $attachments });
1205
1206   named parameters:
1207   letter - the standard letter hashref
1208   attachments - listref of attachments. each attachment is a hashref of:
1209     type - the mime type, like 'text/plain'
1210     content - the actual attachment
1211     filename - the name of the attachment.
1212
1213   returns your letter object, with the content updated.
1214   This routine picks the I<content> of I<letter> and generates a MIME
1215   email, attaching the passed I<attachments> using Koha::Email. The
1216   content is replaced by the string representation of the MIME object,
1217   and the content-type is updated for later handling.
1218
1219 =cut
1220
1221 sub _add_attachments {
1222     my $params = shift;
1223
1224     my $letter = $params->{letter};
1225     my $attachments = $params->{attachments};
1226     return $letter unless @$attachments;
1227
1228     my $message = Koha::Email->new;
1229
1230     if ( $letter->{is_html} ) {
1231         $message->html_body( _wrap_html( $letter->{content}, $letter->{title} ) );
1232     }
1233     else {
1234         $message->text_body( $letter->{content} );
1235     }
1236
1237     foreach my $attachment ( @$attachments ) {
1238         $message->attach(
1239             Encode::encode( "UTF-8", $attachment->{content} ),
1240             content_type => $attachment->{type} || 'application/octet-stream',
1241             name         => $attachment->{filename},
1242             disposition  => 'attachment',
1243         );
1244     }
1245
1246     $letter->{'content-type'} = SERIALIZED_EMAIL_CONTENT_TYPE;
1247     $letter->{content} = $message->as_string;
1248
1249     return $letter;
1250
1251 }
1252
1253 =head2 _get_unsent_messages
1254
1255   This function's parameter hash reference takes the following
1256   optional named parameters:
1257    message_transport_type: method of message sending (e.g. email, sms, etc.)
1258                            Can be a single string, or an arrayref of strings
1259    borrowernumber        : who the message is to be sent
1260    letter_code           : type of message being sent (e.g. PASSWORD_RESET)
1261                            Can be a single string, or an arrayref of strings
1262    message_id            : the message_id of the message. In that case the sub will return only 1 result
1263    limit                 : maximum number of messages to send
1264
1265   This function returns an array of matching hash referenced rows from
1266   message_queue with some borrower information added.
1267
1268 =cut
1269
1270 sub _get_unsent_messages {
1271     my $params = shift;
1272
1273     my $dbh = C4::Context->dbh();
1274     my $statement = qq{
1275         SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.reply_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code, mq.failure_code
1276         FROM message_queue mq
1277         LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1278         WHERE status = ?
1279     };
1280
1281     my @query_params = ('pending');
1282     if ( ref $params ) {
1283         if ( $params->{'borrowernumber'} ) {
1284             $statement .= ' AND mq.borrowernumber = ? ';
1285             push @query_params, $params->{'borrowernumber'};
1286         }
1287         if ( $params->{'letter_code'} ) {
1288             my @letter_codes = ref $params->{'letter_code'} eq "ARRAY" ? @{$params->{'letter_code'}} : $params->{'letter_code'};
1289             if ( @letter_codes ) {
1290                 my $q = join( ",", ("?") x @letter_codes );
1291                 $statement .= " AND mq.letter_code IN ( $q ) ";
1292                 push @query_params, @letter_codes;
1293             }
1294         }
1295         if ( $params->{'message_transport_type'} ) {
1296             my @types = ref $params->{'message_transport_type'} eq "ARRAY" ? @{$params->{'message_transport_type'}} : $params->{'message_transport_type'};
1297             if ( @types ) {
1298                 my $q = join( ",", ("?") x @types );
1299                 $statement .= " AND message_transport_type IN ( $q ) ";
1300                 push @query_params, @types;
1301             }
1302         }
1303         if ( $params->{message_id} ) {
1304             $statement .= ' AND message_id = ?';
1305             push @query_params, $params->{message_id};
1306         }
1307         if ( $params->{where} ) {
1308             $statement .= " AND $params->{where} ";
1309         }
1310         if ( $params->{'limit'} ) {
1311             $statement .= ' limit ? ';
1312             push @query_params, $params->{'limit'};
1313         }
1314     }
1315
1316     my $sth = $dbh->prepare( $statement );
1317     my $result = $sth->execute( @query_params );
1318     return $sth->fetchall_arrayref({});
1319 }
1320
1321 sub _send_message_by_email {
1322     my $message = shift or return;
1323     my ( $username, $password, $method, $smtp_transports ) = @_;
1324
1325     my $patron;
1326     my $to_address = $message->{'to_address'};
1327     unless ($to_address) {
1328         $patron = Koha::Patrons->find( $message->{borrowernumber} );
1329         unless ($patron) {
1330             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1331             _set_message_status(
1332                 {
1333                     message_id   => $message->{'message_id'},
1334                     status       => 'failed',
1335                     failure_code => 'INVALID_BORNUMBER'
1336                 }
1337             );
1338             return;
1339         }
1340         $to_address = $patron->notice_email_address;
1341         unless ($to_address) {  
1342             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1343             # warning too verbose for this more common case?
1344             _set_message_status(
1345                 {
1346                     message_id   => $message->{'message_id'},
1347                     status       => 'failed',
1348                     failure_code => 'NO_EMAIL'
1349                 }
1350             );
1351             return;
1352         }
1353     }
1354
1355     # Skip this message if we exceed domain limits in this run
1356     if( Koha::Notice::Util->exceeds_limit({ to => $to_address, limits => $domain_limits }) ) {
1357         # Save the to_address if you delay the message so that we dont need to look it up again
1358         _update_message_to_address( $message->{'message_id'}, $to_address )
1359             if !$message->{to_address};
1360         return;
1361     }
1362
1363     my $subject = $message->{'subject'};
1364
1365     my $content = $message->{'content'};
1366     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1367     my $is_html = $content_type =~ m/html/io;
1368
1369     my $branch_email = undef;
1370     my $branch_replyto = undef;
1371     my $branch_returnpath = undef;
1372     my $library;
1373
1374     $patron //= Koha::Patrons->find( $message->{borrowernumber} ); # we might already found him
1375     if ($patron) {
1376         $library           = $patron->library;
1377         $branch_email      = $library->from_email_address;
1378         $branch_replyto    = $library->branchreplyto;
1379         $branch_returnpath = $library->branchreturnpath;
1380     }
1381
1382     # NOTE: Patron may not be defined above so branch_email may be undefined still
1383     # so we need to fallback to KohaAdminEmailAddress as a last resort.
1384     my $from_address =
1385          $message->{'from_address'}
1386       || $branch_email
1387       || C4::Context->preference('KohaAdminEmailAddress');
1388     if( !$from_address ) {
1389         _set_message_status(
1390             {
1391                 message_id   => $message->{'message_id'},
1392                 status       => 'failed',
1393                 failure_code => 'NO_FROM',
1394             }
1395         );
1396         return;
1397     };
1398     my $email;
1399
1400     try {
1401
1402         my $params = {
1403             to => $to_address,
1404             (
1405                 C4::Context->preference('NoticeBcc')
1406                 ? ( bcc => C4::Context->preference('NoticeBcc') )
1407                 : ()
1408             ),
1409             from     => $from_address,
1410             reply_to => $message->{'reply_address'} || $branch_replyto,
1411             sender   => $branch_returnpath,
1412             subject  => "" . $message->{subject}
1413         };
1414
1415         if ( $message->{'content_type'} && $message->{'content_type'} eq SERIALIZED_EMAIL_CONTENT_TYPE ) {
1416
1417             # The message has been previously composed as a valid MIME object
1418             # and serialized as a string on the DB
1419             $email = Koha::Email->new_from_string($content);
1420             $email->create($params);
1421         } else {
1422             $email = Koha::Email->create($params);
1423             if ($is_html) {
1424                 $email->html_body( _wrap_html( $content, $subject ) );
1425             } else {
1426                 $email->text_body($content);
1427             }
1428         }
1429     }
1430     catch {
1431         if ( ref($_) eq 'Koha::Exceptions::BadParameter' ) {
1432             _set_message_status(
1433                 {
1434                     message_id   => $message->{'message_id'},
1435                     status       => 'failed',
1436                     failure_code => "INVALID_EMAIL:".$_->parameter
1437                 }
1438             );
1439         } else {
1440             _set_message_status(
1441                 {
1442                     message_id   => $message->{'message_id'},
1443                     status       => 'failed',
1444                     failure_code => 'UNKNOWN_ERROR'
1445                 }
1446             );
1447         }
1448         return 0;
1449     };
1450     return unless $email;
1451
1452     my $smtp_server;
1453     if ( $library ) {
1454         $smtp_server = $library->smtp_server;
1455     }
1456     else {
1457         $smtp_server = Koha::SMTP::Servers->get_default;
1458     }
1459
1460     if ( $username ) {
1461         $smtp_server->set(
1462             {
1463                 sasl_username => $username,
1464                 sasl_password => $password,
1465             }
1466         );
1467     }
1468
1469 # if initial message address was empty, coming here means that a to address was found and
1470 # queue should be updated; same if to address was overriden by Koha::Email->create
1471     _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1472       if !$message->{to_address}
1473       || $message->{to_address} ne $email->email->header('To');
1474
1475     $smtp_transports->{ $smtp_server->id // 'default' } ||= $smtp_server->transport;
1476     my $smtp_transport = $smtp_transports->{ $smtp_server->id // 'default' };
1477
1478     try {
1479         $email->send_or_die({ transport => $smtp_transport });
1480
1481         _set_message_status(
1482             {
1483                 message_id => $message->{'message_id'},
1484                 status     => 'sent',
1485                 failure_code => ''
1486             }
1487         );
1488         return 1;
1489     }
1490     catch {
1491         _set_message_status(
1492             {
1493                 message_id => $message->{'message_id'},
1494                 status     => 'failed',
1495                 failure_code => 'SENDMAIL'
1496             }
1497         );
1498         carp "$_";
1499         carp "$Mail::Sendmail::error";
1500         return;
1501     };
1502 }
1503
1504 sub _wrap_html {
1505     my ($content, $title) = @_;
1506
1507     my $css = C4::Context->preference("NoticeCSS") || '';
1508     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1509     return <<EOS;
1510 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1511     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1512 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1513 <head>
1514 <title>$title</title>
1515 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1516 $css
1517 </head>
1518 <body>
1519 $content
1520 </body>
1521 </html>
1522 EOS
1523 }
1524
1525 sub _is_duplicate {
1526     my ( $message ) = @_;
1527     my $dbh = C4::Context->dbh;
1528     my $count = $dbh->selectrow_array(q|
1529         SELECT COUNT(*)
1530         FROM message_queue
1531         WHERE message_transport_type = ?
1532         AND borrowernumber = ?
1533         AND letter_code = ?
1534         AND CAST(updated_on AS date) = CAST(NOW() AS date)
1535         AND status="sent"
1536         AND content = ?
1537     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1538     return $count;
1539 }
1540
1541 sub _send_message_by_sms {
1542     my $message = shift or return;
1543     my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1544     _update_message_to_address($message->{message_id}, $patron->smsalertnumber) if $patron;
1545
1546     unless ( $patron and $patron->smsalertnumber ) {
1547         _set_message_status( { message_id => $message->{'message_id'},
1548                                status     => 'failed',
1549                                failure_code => 'MISSING_SMS' } );
1550         return;
1551     }
1552
1553     if ( _is_duplicate( $message ) ) {
1554         _set_message_status(
1555             {
1556                 message_id   => $message->{'message_id'},
1557                 status       => 'failed',
1558                 failure_code => 'DUPLICATE_MESSAGE'
1559             }
1560         );
1561         return;
1562     }
1563
1564     my $success = C4::SMS->send_sms(
1565         {
1566             destination => $patron->smsalertnumber,
1567             message     => $message->{'content'},
1568         }
1569     );
1570
1571     if ($success) {
1572         _set_message_status(
1573             {
1574                 message_id   => $message->{'message_id'},
1575                 status       => 'sent',
1576                 failure_code => ''
1577             }
1578         );
1579     }
1580     else {
1581         _set_message_status(
1582             {
1583                 message_id   => $message->{'message_id'},
1584                 status       => 'failed',
1585                 failure_code => 'NO_NOTES'
1586             }
1587         );
1588     }
1589
1590     return $success;
1591 }
1592
1593 sub _update_message_to_address {
1594     my ($id, $to)= @_;
1595     my $dbh = C4::Context->dbh();
1596     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1597 }
1598
1599 sub _update_message_from_address {
1600     my ($message_id, $from_address) = @_;
1601     my $dbh = C4::Context->dbh();
1602     $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1603 }
1604
1605 sub _set_message_status {
1606     my $params = shift or return;
1607
1608     foreach my $required_parameter ( qw( message_id status ) ) {
1609         return unless exists $params->{ $required_parameter };
1610     }
1611
1612     my $dbh = C4::Context->dbh();
1613     my $statement = 'UPDATE message_queue SET status= ?, failure_code= ? WHERE message_id = ?';
1614     my $sth = $dbh->prepare( $statement );
1615     my $result = $sth->execute( $params->{'status'},
1616                                 $params->{'failure_code'} || '',
1617                                 $params->{'message_id'} );
1618     return $result;
1619 }
1620
1621 sub _process_tt {
1622     my ( $params ) = @_;
1623
1624     my $content    = $params->{content};
1625     my $tables     = $params->{tables};
1626     my $loops      = $params->{loops};
1627     my $objects    = $params->{objects} || {};
1628     my $substitute = $params->{substitute} || {};
1629     my $lang = defined($params->{lang}) && $params->{lang} ne 'default' ? $params->{lang} : 'en';
1630     my ($theme, $availablethemes);
1631
1632     my $htdocs = C4::Context->config('intrahtdocs');
1633     ($theme, $lang, $availablethemes)= C4::Templates::availablethemes( $htdocs, 'about.tt', 'intranet', $lang);
1634     my @includes;
1635     foreach (@$availablethemes) {
1636         push @includes, "$htdocs/$_/$lang/includes";
1637         push @includes, "$htdocs/$_/en/includes" unless $lang eq 'en';
1638     }
1639
1640     my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1641     my $template           = Template->new(
1642         {
1643             EVAL_PERL    => 1,
1644             ABSOLUTE     => 1,
1645             PLUGIN_BASE  => 'Koha::Template::Plugin',
1646             COMPILE_EXT  => $use_template_cache ? '.ttc' : '',
1647             COMPILE_DIR  => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1648             INCLUDE_PATH => \@includes,
1649             FILTERS      => {},
1650             ENCODING     => 'UTF-8',
1651         }
1652     ) or die Template->error();
1653
1654     my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute, %$objects };
1655
1656     $content = add_tt_filters( $content );
1657     $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1658
1659     my $output;
1660     my $schema = Koha::Database->new->schema;
1661     $schema->txn_begin;
1662     my $processed = try {
1663         $template->process( \$content, $tt_params, \$output );
1664     }
1665     finally {
1666         $schema->txn_rollback;
1667     };
1668     croak "ERROR PROCESSING TEMPLATE: " . $template->error() unless $processed;
1669
1670     return $output;
1671 }
1672
1673 sub _get_tt_params {
1674     my ($tables, $is_a_loop) = @_;
1675
1676     my $params;
1677     $is_a_loop ||= 0;
1678
1679     my $config = {
1680         article_requests => {
1681             module   => 'Koha::ArticleRequests',
1682             singular => 'article_request',
1683             plural   => 'article_requests',
1684             pk       => 'id',
1685         },
1686         aqbasket => {
1687             module   => 'Koha::Acquisition::Baskets',
1688             singular => 'basket',
1689             plural   => 'baskets',
1690             pk       => 'basketno',
1691         },
1692         aqbooksellers => {
1693             module   => 'Koha::Acquisition::Booksellers',
1694             singular => 'bookseller',
1695             plural   => 'booksellers',
1696             pk       => 'id',
1697         },
1698         biblio => {
1699             module   => 'Koha::Biblios',
1700             singular => 'biblio',
1701             plural   => 'biblios',
1702             pk       => 'biblionumber',
1703         },
1704         biblioitems => {
1705             module   => 'Koha::Biblioitems',
1706             singular => 'biblioitem',
1707             plural   => 'biblioitems',
1708             pk       => 'biblioitemnumber',
1709         },
1710         borrowers => {
1711             module   => 'Koha::Patrons',
1712             singular => 'borrower',
1713             plural   => 'borrowers',
1714             pk       => 'borrowernumber',
1715         },
1716         branches => {
1717             module   => 'Koha::Libraries',
1718             singular => 'branch',
1719             plural   => 'branches',
1720             pk       => 'branchcode',
1721         },
1722         credits => {
1723             module => 'Koha::Account::Lines',
1724             singular => 'credit',
1725             plural => 'credits',
1726             pk => 'accountlines_id',
1727         },
1728         debits => {
1729             module => 'Koha::Account::Lines',
1730             singular => 'debit',
1731             plural => 'debits',
1732             pk => 'accountlines_id',
1733         },
1734         items => {
1735             module   => 'Koha::Items',
1736             singular => 'item',
1737             plural   => 'items',
1738             pk       => 'itemnumber',
1739         },
1740         additional_contents => {
1741             module   => 'Koha::AdditionalContents',
1742             singular => 'additional_content',
1743             plural   => 'additional_contents',
1744             pk       => 'idnew',
1745         },
1746         opac_news => {
1747             module   => 'Koha::AdditionalContents',
1748             singular => 'news',
1749             plural   => 'news',
1750             pk       => 'idnew',
1751         },
1752         aqorders => {
1753             module   => 'Koha::Acquisition::Orders',
1754             singular => 'order',
1755             plural   => 'orders',
1756             pk       => 'ordernumber',
1757         },
1758         reserves => {
1759             module   => 'Koha::Holds',
1760             singular => 'hold',
1761             plural   => 'holds',
1762             pk       => 'reserve_id',
1763         },
1764         serial => {
1765             module   => 'Koha::Serials',
1766             singular => 'serial',
1767             plural   => 'serials',
1768             pk       => 'serialid',
1769         },
1770         subscription => {
1771             module   => 'Koha::Subscriptions',
1772             singular => 'subscription',
1773             plural   => 'subscriptions',
1774             pk       => 'subscriptionid',
1775         },
1776         suggestions => {
1777             module   => 'Koha::Suggestions',
1778             singular => 'suggestion',
1779             plural   => 'suggestions',
1780             pk       => 'suggestionid',
1781         },
1782         tickets => {
1783             module   => 'Koha::Tickets',
1784             singular => 'ticket',
1785             plural   => 'tickets',
1786             pk       => 'id',
1787         },
1788         ticket_updates => {
1789             module   => 'Koha::Ticket::Updates',
1790             singular => 'ticket_update',
1791             plural   => 'ticket_updates',
1792             pk       => 'id',
1793         },
1794         issues => {
1795             module   => 'Koha::Checkouts',
1796             singular => 'checkout',
1797             plural   => 'checkouts',
1798             fk       => 'itemnumber',
1799         },
1800         old_issues => {
1801             module   => 'Koha::Old::Checkouts',
1802             singular => 'old_checkout',
1803             plural   => 'old_checkouts',
1804             pk       => 'issue_id',
1805         },
1806         overdues => {
1807             module   => 'Koha::Checkouts',
1808             singular => 'overdue',
1809             plural   => 'overdues',
1810             fk       => 'itemnumber',
1811         },
1812         borrower_modifications => {
1813             module   => 'Koha::Patron::Modifications',
1814             singular => 'patron_modification',
1815             plural   => 'patron_modifications',
1816             fk       => 'verification_token',
1817         },
1818         illrequests => {
1819             module   => 'Koha::Illrequests',
1820             singular => 'illrequest',
1821             plural   => 'illrequests',
1822             pk       => 'illrequest_id'
1823         }
1824     };
1825
1826     foreach my $table ( keys %$tables ) {
1827         next unless $config->{$table};
1828
1829         my $ref = ref( $tables->{$table} ) || q{};
1830         my $module = $config->{$table}->{module};
1831
1832         if ( can_load( modules => { $module => undef } ) ) {
1833             my $pk = $config->{$table}->{pk};
1834             my $fk = $config->{$table}->{fk};
1835
1836             if ( $is_a_loop ) {
1837                 my $values = $tables->{$table} || [];
1838                 unless ( ref( $values ) eq 'ARRAY' ) {
1839                     croak "ERROR processing table $table. Wrong API call.";
1840                 }
1841                 my $key = $pk ? $pk : $fk;
1842                 # $key does not come from user input
1843                 my $objects = $module->search(
1844                     { $key => $values },
1845                     {
1846                             # We want to retrieve the data in the same order
1847                             # FIXME MySQLism
1848                             # field is a MySQLism, but they are no other way to do it
1849                             # To be generic we could do it in perl, but we will need to fetch
1850                             # all the data then order them
1851                         @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1852                     }
1853                 );
1854                 $params->{ $config->{$table}->{plural} } = $objects;
1855             }
1856             elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1857                 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1858                 my $object;
1859                 if ( $fk ) { # Using a foreign key for lookup
1860                     if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1861                         my $search;
1862                         foreach my $key ( @$fk ) {
1863                             $search->{$key} = $id->{$key};
1864                         }
1865                         $object = $module->search( $search )->last();
1866                     } else { # Foreign key is single column
1867                         $object = $module->search( { $fk => $id } )->last();
1868                     }
1869                 } else { # using the table's primary key for lookup
1870                     $object = $module->find($id);
1871                 }
1872                 $params->{ $config->{$table}->{singular} } = $object;
1873             }
1874             else {    # $ref eq 'ARRAY'
1875                 my $object;
1876                 if ( @{ $tables->{$table} } == 1 ) {    # Param is a single key
1877                     $object = $module->search( { $pk => $tables->{$table} } )->last();
1878                 }
1879                 else {                                  # Params are mutliple foreign keys
1880                     croak "Multiple foreign keys (table $table) should be passed using an hashref";
1881                 }
1882                 $params->{ $config->{$table}->{singular} } = $object;
1883             }
1884         }
1885         else {
1886             croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1887         }
1888     }
1889
1890     $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1891
1892     return $params;
1893 }
1894
1895 =head3 add_tt_filters
1896
1897 $content = add_tt_filters( $content );
1898
1899 Add TT filters to some specific fields if needed.
1900
1901 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1902
1903 =cut
1904
1905 sub add_tt_filters {
1906     my ( $content ) = @_;
1907     $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1908     $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1909     return $content;
1910 }
1911
1912 =head2 get_item_content
1913
1914     my $item = Koha::Items->find(...)->unblessed;
1915     my @item_content_fields = qw( date_due title barcode author itemnumber );
1916     my $item_content = C4::Letters::get_item_content({
1917                              item => $item,
1918                              item_content_fields => \@item_content_fields
1919                        });
1920
1921 This function generates a tab-separated list of values for the passed item. Dates
1922 are formatted following the current setup.
1923
1924 =cut
1925
1926 sub get_item_content {
1927     my ( $params ) = @_;
1928     my $item = $params->{item};
1929     my $dateonly = $params->{dateonly} || 0;
1930     my $item_content_fields = $params->{item_content_fields} || [];
1931
1932     return unless $item;
1933
1934     my @item_info = map {
1935         $_ =~ /^date|date$/
1936           ? eval {
1937             output_pref(
1938                 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1939           }
1940           : $item->{$_}
1941           || ''
1942     } @$item_content_fields;
1943     return join( "\t", @item_info ) . "\n";
1944 }
1945
1946 1;
1947 __END__