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