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