Bug 27324: Use Koha.Preference() for intranetbookbag everywhere
[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|0000-00-00|
864             and not $replacedby =~ m|9999-12-31|
865             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
866         {
867             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
868             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
869             my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
870
871             for my $letter_field ( qw( title content ) ) {
872                 my $filter_string_used = q{};
873                 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
874                     # We overwrite $dateonly if the filter exists and we have a time in the datetime
875                     $filter_string_used = $1 || q{};
876                     $dateonly = $1 unless $dateonly;
877                 }
878                 my $replacedby_date = eval {
879                     output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
880                 };
881
882                 if ( $letter->{ $letter_field } ) {
883                     $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
884                     $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
885                 }
886             }
887         }
888         # Other fields replacement
889         else {
890             for my $letter_field ( qw( title content ) ) {
891                 if ( $letter->{ $letter_field } ) {
892                     $letter->{ $letter_field }   =~ s/<<$table.$field>>/$replacedby/g;
893                     $letter->{ $letter_field }   =~ s/<<$field>>/$replacedby/g;
894                 }
895             }
896         }
897     }
898
899     if ($table eq 'borrowers' && $letter->{content}) {
900         my $patron = Koha::Patrons->find( $values->{borrowernumber} );
901         if ( $patron ) {
902             my $attributes = $patron->extended_attributes;
903             my %attr;
904             while ( my $attribute = $attributes->next ) {
905                 my $code = $attribute->code;
906                 my $val  = $attribute->description; # FIXME - we always display intranet description here!
907                 $val =~ s/\p{P}(?=$)//g if $val;
908                 next unless $val gt '';
909                 $attr{$code} ||= [];
910                 push @{ $attr{$code} }, $val;
911             }
912             while ( my ($code, $val_ar) = each %attr ) {
913                 my $replacefield = "<<borrower-attribute:$code>>";
914                 my $replacedby   = join ',', @$val_ar;
915                 $letter->{content} =~ s/$replacefield/$replacedby/g;
916             }
917         }
918     }
919     return $letter;
920 }
921
922 =head2 EnqueueLetter
923
924   my $success = EnqueueLetter( { letter => $letter, 
925         borrowernumber => '12', message_transport_type => 'email' } )
926
927 places a letter in the message_queue database table, which will
928 eventually get processed (sent) by the process_message_queue.pl
929 cronjob when it calls SendQueuedMessages.
930
931 return message_id on success
932
933 =cut
934
935 sub EnqueueLetter {
936     my $params = shift or return;
937
938     return unless exists $params->{'letter'};
939 #   return unless exists $params->{'borrowernumber'};
940     return unless exists $params->{'message_transport_type'};
941
942     my $content = $params->{letter}->{content};
943     $content =~ s/\s+//g if(defined $content);
944     if ( not defined $content or $content eq '' ) {
945         warn "Trying to add an empty message to the message queue" if $debug;
946         return;
947     }
948
949     # If we have any attachments we should encode then into the body.
950     if ( $params->{'attachments'} ) {
951         $params->{'letter'} = _add_attachments(
952             {   letter      => $params->{'letter'},
953                 attachments => $params->{'attachments'},
954                 message     => MIME::Lite->new( Type => 'multipart/mixed' ),
955             }
956         );
957     }
958
959     my $dbh       = C4::Context->dbh();
960     my $statement = << 'ENDSQL';
961 INSERT INTO message_queue
962 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type )
963 VALUES
964 ( ?,              ?,       ?,       ?,        ?,           ?,                      ?,      NOW(),       ?,          ?,            ?,           ? )
965 ENDSQL
966
967     my $sth    = $dbh->prepare($statement);
968     my $result = $sth->execute(
969         $params->{'borrowernumber'},              # borrowernumber
970         $params->{'letter'}->{'title'},           # subject
971         $params->{'letter'}->{'content'},         # content
972         $params->{'letter'}->{'metadata'} || '',  # metadata
973         $params->{'letter'}->{'code'}     || '',  # letter_code
974         $params->{'message_transport_type'},      # message_transport_type
975         'pending',                                # status
976         $params->{'to_address'},                  # to_address
977         $params->{'from_address'},                # from_address
978         $params->{'reply_address'},               # reply_address
979         $params->{'letter'}->{'content-type'},    # content_type
980     );
981     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
982 }
983
984 =head2 SendQueuedMessages ([$hashref]) 
985
986     my $sent = SendQueuedMessages({
987         letter_code => $letter_code,
988         borrowernumber => $who_letter_is_for,
989         limit => 50,
990         verbose => 1,
991         type => 'sms',
992     });
993
994 Sends all of the 'pending' items in the message queue, unless
995 parameters are passed.
996
997 The letter_code, borrowernumber and limit parameters are used
998 to build a parameter set for _get_unsent_messages, thus limiting
999 which pending messages will be processed. They are all optional.
1000
1001 The verbose parameter can be used to generate debugging output.
1002 It is also optional.
1003
1004 Returns number of messages sent.
1005
1006 =cut
1007
1008 sub SendQueuedMessages {
1009     my $params = shift;
1010
1011     my $which_unsent_messages  = {
1012         'limit'          => $params->{'limit'} // 0,
1013         'borrowernumber' => $params->{'borrowernumber'} // q{},
1014         'letter_code'    => $params->{'letter_code'} // q{},
1015         'type'           => $params->{'type'} // q{},
1016     };
1017     my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1018     MESSAGE: foreach my $message ( @$unsent_messages ) {
1019         my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
1020         # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
1021         $message_object->make_column_dirty('status');
1022         return unless $message_object->store;
1023
1024         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1025         warn sprintf( 'sending %s message to patron: %s',
1026                       $message->{'message_transport_type'},
1027                       $message->{'borrowernumber'} || 'Admin' )
1028           if $params->{'verbose'} or $debug;
1029         # This is just begging for subclassing
1030         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1031         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1032             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1033         }
1034         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1035             if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1036                 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1037                 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1038                 unless ( $sms_provider ) {
1039                     warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1040                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1041                     next MESSAGE;
1042                 }
1043                 unless ( $patron->smsalertnumber ) {
1044                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1045                     warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1046                     next MESSAGE;
1047                 }
1048                 $message->{to_address}  = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1049                 $message->{to_address} .= '@' . $sms_provider->domain();
1050
1051                 # Check for possible from_address override
1052                 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1053                 if ($from_address && $message->{from_address} ne $from_address) {
1054                     $message->{from_address} = $from_address;
1055                     _update_message_from_address($message->{'message_id'}, $message->{from_address});
1056                 }
1057
1058                 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1059                 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1060             } else {
1061                 _send_message_by_sms( $message );
1062             }
1063         }
1064     }
1065     return scalar( @$unsent_messages );
1066 }
1067
1068 =head2 GetRSSMessages
1069
1070   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1071
1072 returns a listref of all queued RSS messages for a particular person.
1073
1074 =cut
1075
1076 sub GetRSSMessages {
1077     my $params = shift;
1078
1079     return unless $params;
1080     return unless ref $params;
1081     return unless $params->{'borrowernumber'};
1082     
1083     return _get_unsent_messages( { message_transport_type => 'rss',
1084                                    limit                  => $params->{'limit'},
1085                                    borrowernumber         => $params->{'borrowernumber'}, } );
1086 }
1087
1088 =head2 GetPrintMessages
1089
1090   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1091
1092 Returns a arrayref of all queued print messages (optionally, for a particular
1093 person).
1094
1095 =cut
1096
1097 sub GetPrintMessages {
1098     my $params = shift || {};
1099     
1100     return _get_unsent_messages( { message_transport_type => 'print',
1101                                    borrowernumber         => $params->{'borrowernumber'},
1102                                  } );
1103 }
1104
1105 =head2 GetQueuedMessages ([$hashref])
1106
1107   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1108
1109 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1110 and limited to specified limit.
1111
1112 Return is an arrayref of hashes, each has represents a message in the message queue.
1113
1114 =cut
1115
1116 sub GetQueuedMessages {
1117     my $params = shift;
1118
1119     my $dbh = C4::Context->dbh();
1120     my $statement = << 'ENDSQL';
1121 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on
1122 FROM message_queue
1123 ENDSQL
1124
1125     my @query_params;
1126     my @whereclauses;
1127     if ( exists $params->{'borrowernumber'} ) {
1128         push @whereclauses, ' borrowernumber = ? ';
1129         push @query_params, $params->{'borrowernumber'};
1130     }
1131
1132     if ( @whereclauses ) {
1133         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1134     }
1135
1136     if ( defined $params->{'limit'} ) {
1137         $statement .= ' LIMIT ? ';
1138         push @query_params, $params->{'limit'};
1139     }
1140
1141     my $sth = $dbh->prepare( $statement );
1142     my $result = $sth->execute( @query_params );
1143     return $sth->fetchall_arrayref({});
1144 }
1145
1146 =head2 GetMessageTransportTypes
1147
1148   my @mtt = GetMessageTransportTypes();
1149
1150   returns an arrayref of transport types
1151
1152 =cut
1153
1154 sub GetMessageTransportTypes {
1155     my $dbh = C4::Context->dbh();
1156     my $mtts = $dbh->selectcol_arrayref("
1157         SELECT message_transport_type
1158         FROM message_transport_types
1159         ORDER BY message_transport_type
1160     ");
1161     return $mtts;
1162 }
1163
1164 =head2 GetMessage
1165
1166     my $message = C4::Letters::Message($message_id);
1167
1168 =cut
1169
1170 sub GetMessage {
1171     my ( $message_id ) = @_;
1172     return unless $message_id;
1173     my $dbh = C4::Context->dbh;
1174     return $dbh->selectrow_hashref(q|
1175         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
1176         FROM message_queue
1177         WHERE message_id = ?
1178     |, {}, $message_id );
1179 }
1180
1181 =head2 ResendMessage
1182
1183   Attempt to resend a message which has failed previously.
1184
1185   my $has_been_resent = C4::Letters::ResendMessage($message_id);
1186
1187   Updates the message to 'pending' status so that
1188   it will be resent later on.
1189
1190   returns 1 on success, 0 on failure, undef if no message was found
1191
1192 =cut
1193
1194 sub ResendMessage {
1195     my $message_id = shift;
1196     return unless $message_id;
1197
1198     my $message = GetMessage( $message_id );
1199     return unless $message;
1200     my $rv = 0;
1201     if ( $message->{status} ne 'pending' ) {
1202         $rv = C4::Letters::_set_message_status({
1203             message_id => $message_id,
1204             status => 'pending',
1205         });
1206         $rv = $rv > 0? 1: 0;
1207         # Clear destination email address to force address update
1208         _update_message_to_address( $message_id, undef ) if $rv &&
1209             $message->{message_transport_type} eq 'email';
1210     }
1211     return $rv;
1212 }
1213
1214 =head2 _add_attachements
1215
1216   named parameters:
1217   letter - the standard letter hashref
1218   attachments - listref of attachments. each attachment is a hashref of:
1219     type - the mime type, like 'text/plain'
1220     content - the actual attachment
1221     filename - the name of the attachment.
1222   message - a MIME::Lite object to attach these to.
1223
1224   returns your letter object, with the content updated.
1225
1226 =cut
1227
1228 sub _add_attachments {
1229     my $params = shift;
1230
1231     my $letter = $params->{'letter'};
1232     my $attachments = $params->{'attachments'};
1233     return $letter unless @$attachments;
1234     my $message = $params->{'message'};
1235
1236     # First, we have to put the body in as the first attachment
1237     $message->attach(
1238         Type => $letter->{'content-type'} || 'TEXT',
1239         Data => $letter->{'is_html'}
1240             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1241             : $letter->{'content'},
1242     );
1243
1244     foreach my $attachment ( @$attachments ) {
1245         $message->attach(
1246             Type     => $attachment->{'type'},
1247             Data     => $attachment->{'content'},
1248             Filename => $attachment->{'filename'},
1249         );
1250     }
1251     # we're forcing list context here to get the header, not the count back from grep.
1252     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1253     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1254     $letter->{'content'} = $message->body_as_string;
1255
1256     return $letter;
1257
1258 }
1259
1260 =head2 _get_unsent_messages
1261
1262   This function's parameter hash reference takes the following
1263   optional named parameters:
1264    message_transport_type: method of message sending (e.g. email, sms, etc.)
1265    borrowernumber        : who the message is to be sent
1266    letter_code           : type of message being sent (e.g. PASSWORD_RESET)
1267    limit                 : maximum number of messages to send
1268
1269   This function returns an array of matching hash referenced rows from
1270   message_queue with some borrower information added.
1271
1272 =cut
1273
1274 sub _get_unsent_messages {
1275     my $params = shift;
1276
1277     my $dbh = C4::Context->dbh();
1278     my $statement = qq{
1279         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
1280         FROM message_queue mq
1281         LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1282         WHERE status = ?
1283     };
1284
1285     my @query_params = ('pending');
1286     if ( ref $params ) {
1287         if ( $params->{'message_transport_type'} ) {
1288             $statement .= ' AND mq.message_transport_type = ? ';
1289             push @query_params, $params->{'message_transport_type'};
1290         }
1291         if ( $params->{'borrowernumber'} ) {
1292             $statement .= ' AND mq.borrowernumber = ? ';
1293             push @query_params, $params->{'borrowernumber'};
1294         }
1295         if ( $params->{'letter_code'} ) {
1296             $statement .= ' AND mq.letter_code = ? ';
1297             push @query_params, $params->{'letter_code'};
1298         }
1299         if ( $params->{'type'} ) {
1300             $statement .= ' AND message_transport_type = ? ';
1301             push @query_params, $params->{'type'};
1302         }
1303         if ( $params->{'limit'} ) {
1304             $statement .= ' limit ? ';
1305             push @query_params, $params->{'limit'};
1306         }
1307     }
1308
1309     $debug and warn "_get_unsent_messages SQL: $statement";
1310     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1311     my $sth = $dbh->prepare( $statement );
1312     my $result = $sth->execute( @query_params );
1313     return $sth->fetchall_arrayref({});
1314 }
1315
1316 sub _send_message_by_email {
1317     my $message = shift or return;
1318     my ($username, $password, $method) = @_;
1319
1320     my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1321     my $to_address = $message->{'to_address'};
1322     unless ($to_address) {
1323         unless ($patron) {
1324             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1325             _set_message_status( { message_id => $message->{'message_id'},
1326                                    status     => 'failed' } );
1327             return;
1328         }
1329         $to_address = $patron->notice_email_address;
1330         unless ($to_address) {  
1331             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1332             # warning too verbose for this more common case?
1333             _set_message_status( { message_id => $message->{'message_id'},
1334                                    status     => 'failed' } );
1335             return;
1336         }
1337     }
1338
1339     my $subject = $message->{'subject'};
1340
1341     my $content = $message->{'content'};
1342     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1343     my $is_html = $content_type =~ m/html/io;
1344
1345     my $branch_email = undef;
1346     my $branch_replyto = undef;
1347     my $branch_returnpath = undef;
1348     my $library;
1349
1350     if ($patron) {
1351         $library           = $patron->library;
1352         $branch_email      = $library->branchemail;
1353         $branch_replyto    = $library->branchreplyto;
1354         $branch_returnpath = $library->branchreturnpath;
1355     }
1356
1357     my $email = Koha::Email->create(
1358         {
1359             to => $to_address,
1360             (
1361                 C4::Context->preference('NoticeBcc')
1362                 ? ( bcc => C4::Context->preference('NoticeBcc') )
1363                 : ()
1364             ),
1365             from     => $message->{'from_address'}  || $branch_email,
1366             reply_to => $message->{'reply_address'} || $branch_replyto,
1367             sender   => $branch_returnpath,
1368             subject  => "" . $message->{subject}
1369         }
1370     );
1371
1372     if ( $is_html ) {
1373         $email->html_body(
1374             _wrap_html( $content, $subject )
1375         );
1376     }
1377     else {
1378         $email->text_body( $content );
1379     }
1380
1381     my $smtp_server;
1382     if ( $library ) {
1383         $smtp_server = $library->smtp_server;
1384     }
1385     else {
1386         $smtp_server = Koha::SMTP::Servers->get_default;
1387     }
1388
1389     if ( $username ) {
1390         $smtp_server->set(
1391             {
1392                 sasl_username => $username,
1393                 sasl_password => $password,
1394             }
1395         );
1396     }
1397
1398 # if initial message address was empty, coming here means that a to address was found and
1399 # queue should be updated; same if to address was overriden by Koha::Email->create
1400     _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1401       if !$message->{to_address}
1402       || $message->{to_address} ne $email->email->header('To');
1403
1404     try {
1405         $email->send_or_die({ transport => $smtp_server->transport });
1406
1407         _set_message_status(
1408             {
1409                 message_id => $message->{'message_id'},
1410                 status     => 'sent'
1411             }
1412         );
1413         return 1;
1414     }
1415     catch {
1416         _set_message_status(
1417             {
1418                 message_id => $message->{'message_id'},
1419                 status     => 'failed'
1420             }
1421         );
1422         carp "$_";
1423         return;
1424     };
1425 }
1426
1427 sub _wrap_html {
1428     my ($content, $title) = @_;
1429
1430     my $css = C4::Context->preference("NoticeCSS") || '';
1431     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1432     return <<EOS;
1433 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1434     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1435 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1436 <head>
1437 <title>$title</title>
1438 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1439 $css
1440 </head>
1441 <body>
1442 $content
1443 </body>
1444 </html>
1445 EOS
1446 }
1447
1448 sub _is_duplicate {
1449     my ( $message ) = @_;
1450     my $dbh = C4::Context->dbh;
1451     my $count = $dbh->selectrow_array(q|
1452         SELECT COUNT(*)
1453         FROM message_queue
1454         WHERE message_transport_type = ?
1455         AND borrowernumber = ?
1456         AND letter_code = ?
1457         AND CAST(updated_on AS date) = CAST(NOW() AS date)
1458         AND status="sent"
1459         AND content = ?
1460     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1461     return $count;
1462 }
1463
1464 sub _send_message_by_sms {
1465     my $message = shift or return;
1466     my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1467
1468     unless ( $patron and $patron->smsalertnumber ) {
1469         _set_message_status( { message_id => $message->{'message_id'},
1470                                status     => 'failed' } );
1471         return;
1472     }
1473
1474     if ( _is_duplicate( $message ) ) {
1475         _set_message_status( { message_id => $message->{'message_id'},
1476                                status     => 'failed' } );
1477         return;
1478     }
1479
1480     my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1481                                        message     => $message->{'content'},
1482                                      } );
1483     _set_message_status( { message_id => $message->{'message_id'},
1484                            status     => ($success ? 'sent' : 'failed') } );
1485     return $success;
1486 }
1487
1488 sub _update_message_to_address {
1489     my ($id, $to)= @_;
1490     my $dbh = C4::Context->dbh();
1491     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1492 }
1493
1494 sub _update_message_from_address {
1495     my ($message_id, $from_address) = @_;
1496     my $dbh = C4::Context->dbh();
1497     $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1498 }
1499
1500 sub _set_message_status {
1501     my $params = shift or return;
1502
1503     foreach my $required_parameter ( qw( message_id status ) ) {
1504         return unless exists $params->{ $required_parameter };
1505     }
1506
1507     my $dbh = C4::Context->dbh();
1508     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1509     my $sth = $dbh->prepare( $statement );
1510     my $result = $sth->execute( $params->{'status'},
1511                                 $params->{'message_id'} );
1512     return $result;
1513 }
1514
1515 sub _process_tt {
1516     my ( $params ) = @_;
1517
1518     my $content = $params->{content};
1519     my $tables = $params->{tables};
1520     my $loops = $params->{loops};
1521     my $substitute = $params->{substitute} || {};
1522
1523     my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1524     my $template           = Template->new(
1525         {
1526             EVAL_PERL    => 1,
1527             ABSOLUTE     => 1,
1528             PLUGIN_BASE  => 'Koha::Template::Plugin',
1529             COMPILE_EXT  => $use_template_cache ? '.ttc' : '',
1530             COMPILE_DIR  => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1531             FILTERS      => {},
1532             ENCODING     => 'UTF-8',
1533         }
1534     ) or die Template->error();
1535
1536     my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1537
1538     $content = add_tt_filters( $content );
1539     $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1540
1541     my $output;
1542     $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1543
1544     return $output;
1545 }
1546
1547 sub _get_tt_params {
1548     my ($tables, $is_a_loop) = @_;
1549
1550     my $params;
1551     $is_a_loop ||= 0;
1552
1553     my $config = {
1554         article_requests => {
1555             module   => 'Koha::ArticleRequests',
1556             singular => 'article_request',
1557             plural   => 'article_requests',
1558             pk       => 'id',
1559         },
1560         aqbasket => {
1561             module   => 'Koha::Acquisition::Baskets',
1562             singular => 'basket',
1563             plural   => 'baskets',
1564             pk       => 'basketno',
1565         },
1566         biblio => {
1567             module   => 'Koha::Biblios',
1568             singular => 'biblio',
1569             plural   => 'biblios',
1570             pk       => 'biblionumber',
1571         },
1572         biblioitems => {
1573             module   => 'Koha::Biblioitems',
1574             singular => 'biblioitem',
1575             plural   => 'biblioitems',
1576             pk       => 'biblioitemnumber',
1577         },
1578         borrowers => {
1579             module   => 'Koha::Patrons',
1580             singular => 'borrower',
1581             plural   => 'borrowers',
1582             pk       => 'borrowernumber',
1583         },
1584         branches => {
1585             module   => 'Koha::Libraries',
1586             singular => 'branch',
1587             plural   => 'branches',
1588             pk       => 'branchcode',
1589         },
1590         items => {
1591             module   => 'Koha::Items',
1592             singular => 'item',
1593             plural   => 'items',
1594             pk       => 'itemnumber',
1595         },
1596         opac_news => {
1597             module   => 'Koha::News',
1598             singular => 'news',
1599             plural   => 'news',
1600             pk       => 'idnew',
1601         },
1602         aqorders => {
1603             module   => 'Koha::Acquisition::Orders',
1604             singular => 'order',
1605             plural   => 'orders',
1606             pk       => 'ordernumber',
1607         },
1608         reserves => {
1609             module   => 'Koha::Holds',
1610             singular => 'hold',
1611             plural   => 'holds',
1612             pk       => 'reserve_id',
1613         },
1614         serial => {
1615             module   => 'Koha::Serials',
1616             singular => 'serial',
1617             plural   => 'serials',
1618             pk       => 'serialid',
1619         },
1620         subscription => {
1621             module   => 'Koha::Subscriptions',
1622             singular => 'subscription',
1623             plural   => 'subscriptions',
1624             pk       => 'subscriptionid',
1625         },
1626         suggestions => {
1627             module   => 'Koha::Suggestions',
1628             singular => 'suggestion',
1629             plural   => 'suggestions',
1630             pk       => 'suggestionid',
1631         },
1632         issues => {
1633             module   => 'Koha::Checkouts',
1634             singular => 'checkout',
1635             plural   => 'checkouts',
1636             fk       => 'itemnumber',
1637         },
1638         old_issues => {
1639             module   => 'Koha::Old::Checkouts',
1640             singular => 'old_checkout',
1641             plural   => 'old_checkouts',
1642             fk       => 'itemnumber',
1643         },
1644         overdues => {
1645             module   => 'Koha::Checkouts',
1646             singular => 'overdue',
1647             plural   => 'overdues',
1648             fk       => 'itemnumber',
1649         },
1650         borrower_modifications => {
1651             module   => 'Koha::Patron::Modifications',
1652             singular => 'patron_modification',
1653             plural   => 'patron_modifications',
1654             fk       => 'verification_token',
1655         },
1656         illrequests => {
1657             module   => 'Koha::Illrequests',
1658             singular => 'illrequest',
1659             plural   => 'illrequests',
1660             pk       => 'illrequest_id'
1661         }
1662     };
1663
1664     foreach my $table ( keys %$tables ) {
1665         next unless $config->{$table};
1666
1667         my $ref = ref( $tables->{$table} ) || q{};
1668         my $module = $config->{$table}->{module};
1669
1670         if ( can_load( modules => { $module => undef } ) ) {
1671             my $pk = $config->{$table}->{pk};
1672             my $fk = $config->{$table}->{fk};
1673
1674             if ( $is_a_loop ) {
1675                 my $values = $tables->{$table} || [];
1676                 unless ( ref( $values ) eq 'ARRAY' ) {
1677                     croak "ERROR processing table $table. Wrong API call.";
1678                 }
1679                 my $key = $pk ? $pk : $fk;
1680                 # $key does not come from user input
1681                 my $objects = $module->search(
1682                     { $key => $values },
1683                     {
1684                             # We want to retrieve the data in the same order
1685                             # FIXME MySQLism
1686                             # field is a MySQLism, but they are no other way to do it
1687                             # To be generic we could do it in perl, but we will need to fetch
1688                             # all the data then order them
1689                         @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1690                     }
1691                 );
1692                 $params->{ $config->{$table}->{plural} } = $objects;
1693             }
1694             elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1695                 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1696                 my $object;
1697                 if ( $fk ) { # Using a foreign key for lookup
1698                     if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1699                         my $search;
1700                         foreach my $key ( @$fk ) {
1701                             $search->{$key} = $id->{$key};
1702                         }
1703                         $object = $module->search( $search )->last();
1704                     } else { # Foreign key is single column
1705                         $object = $module->search( { $fk => $id } )->last();
1706                     }
1707                 } else { # using the table's primary key for lookup
1708                     $object = $module->find($id);
1709                 }
1710                 $params->{ $config->{$table}->{singular} } = $object;
1711             }
1712             else {    # $ref eq 'ARRAY'
1713                 my $object;
1714                 if ( @{ $tables->{$table} } == 1 ) {    # Param is a single key
1715                     $object = $module->search( { $pk => $tables->{$table} } )->last();
1716                 }
1717                 else {                                  # Params are mutliple foreign keys
1718                     croak "Multiple foreign keys (table $table) should be passed using an hashref";
1719                 }
1720                 $params->{ $config->{$table}->{singular} } = $object;
1721             }
1722         }
1723         else {
1724             croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1725         }
1726     }
1727
1728     $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1729
1730     return $params;
1731 }
1732
1733 =head3 add_tt_filters
1734
1735 $content = add_tt_filters( $content );
1736
1737 Add TT filters to some specific fields if needed.
1738
1739 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1740
1741 =cut
1742
1743 sub add_tt_filters {
1744     my ( $content ) = @_;
1745     $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1746     $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1747     return $content;
1748 }
1749
1750 =head2 get_item_content
1751
1752     my $item = Koha::Items->find(...)->unblessed;
1753     my @item_content_fields = qw( date_due title barcode author itemnumber );
1754     my $item_content = C4::Letters::get_item_content({
1755                              item => $item,
1756                              item_content_fields => \@item_content_fields
1757                        });
1758
1759 This function generates a tab-separated list of values for the passed item. Dates
1760 are formatted following the current setup.
1761
1762 =cut
1763
1764 sub get_item_content {
1765     my ( $params ) = @_;
1766     my $item = $params->{item};
1767     my $dateonly = $params->{dateonly} || 0;
1768     my $item_content_fields = $params->{item_content_fields} || [];
1769
1770     return unless $item;
1771
1772     my @item_info = map {
1773         $_ =~ /^date|date$/
1774           ? eval {
1775             output_pref(
1776                 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1777           }
1778           : $item->{$_}
1779           || ''
1780     } @$item_content_fields;
1781     return join( "\t", @item_info ) . "\n";
1782 }
1783
1784 1;
1785 __END__