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