Bug 18570: (QA follow-up) Improved POD
[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({
1030         letter_code => $letter_code,
1031         borrowernumber => $who_letter_is_for,
1032         limit => 50,
1033         verbose => 1
1034     });
1035
1036 Sends all of the 'pending' items in the message queue, unless
1037 parameters are passed.
1038
1039 The letter_code, borrowernumber and limit parameters are used
1040 to build a parameter set for _get_unsent_messages, thus limiting
1041 which pending messages will be processed. They are all optional.
1042
1043 The verbose parameter can be used to generate debugging output.
1044 It is also optional.
1045
1046 Returns number of messages sent.
1047
1048 =cut
1049
1050 sub SendQueuedMessages {
1051     my $params = shift;
1052
1053     my $which_unsent_messages  = {
1054         'limit'          => $params->{'limit'} // 0,
1055         'borrowernumber' => $params->{'borrowernumber'} // q{},
1056         'letter_code'    => $params->{'letter_code'} // q{},
1057     };
1058     my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
1059     MESSAGE: foreach my $message ( @$unsent_messages ) {
1060         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
1061         warn sprintf( 'sending %s message to patron: %s',
1062                       $message->{'message_transport_type'},
1063                       $message->{'borrowernumber'} || 'Admin' )
1064           if $params->{'verbose'} or $debug;
1065         # This is just begging for subclassing
1066         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
1067         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
1068             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1069         }
1070         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1071             if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1072                 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1073                 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1074                 unless ( $sms_provider ) {
1075                     warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1076                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1077                     next MESSAGE;
1078                 }
1079                 unless ( $patron->smsalertnumber ) {
1080                     _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1081                     warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1082                     next MESSAGE;
1083                 }
1084                 $message->{to_address}  = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1085                 $message->{to_address} .= '@' . $sms_provider->domain();
1086                 _update_message_to_address($message->{'message_id'},$message->{to_address});
1087                 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1088             } else {
1089                 _send_message_by_sms( $message );
1090             }
1091         }
1092     }
1093     return scalar( @$unsent_messages );
1094 }
1095
1096 =head2 GetRSSMessages
1097
1098   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1099
1100 returns a listref of all queued RSS messages for a particular person.
1101
1102 =cut
1103
1104 sub GetRSSMessages {
1105     my $params = shift;
1106
1107     return unless $params;
1108     return unless ref $params;
1109     return unless $params->{'borrowernumber'};
1110     
1111     return _get_unsent_messages( { message_transport_type => 'rss',
1112                                    limit                  => $params->{'limit'},
1113                                    borrowernumber         => $params->{'borrowernumber'}, } );
1114 }
1115
1116 =head2 GetPrintMessages
1117
1118   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1119
1120 Returns a arrayref of all queued print messages (optionally, for a particular
1121 person).
1122
1123 =cut
1124
1125 sub GetPrintMessages {
1126     my $params = shift || {};
1127     
1128     return _get_unsent_messages( { message_transport_type => 'print',
1129                                    borrowernumber         => $params->{'borrowernumber'},
1130                                  } );
1131 }
1132
1133 =head2 GetQueuedMessages ([$hashref])
1134
1135   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1136
1137 fetches messages out of the message queue.
1138
1139 returns:
1140 list of hashes, each has represents a message in the message queue.
1141
1142 =cut
1143
1144 sub GetQueuedMessages {
1145     my $params = shift;
1146
1147     my $dbh = C4::Context->dbh();
1148     my $statement = << 'ENDSQL';
1149 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1150 FROM message_queue
1151 ENDSQL
1152
1153     my @query_params;
1154     my @whereclauses;
1155     if ( exists $params->{'borrowernumber'} ) {
1156         push @whereclauses, ' borrowernumber = ? ';
1157         push @query_params, $params->{'borrowernumber'};
1158     }
1159
1160     if ( @whereclauses ) {
1161         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1162     }
1163
1164     if ( defined $params->{'limit'} ) {
1165         $statement .= ' LIMIT ? ';
1166         push @query_params, $params->{'limit'};
1167     }
1168
1169     my $sth = $dbh->prepare( $statement );
1170     my $result = $sth->execute( @query_params );
1171     return $sth->fetchall_arrayref({});
1172 }
1173
1174 =head2 GetMessageTransportTypes
1175
1176   my @mtt = GetMessageTransportTypes();
1177
1178   returns an arrayref of transport types
1179
1180 =cut
1181
1182 sub GetMessageTransportTypes {
1183     my $dbh = C4::Context->dbh();
1184     my $mtts = $dbh->selectcol_arrayref("
1185         SELECT message_transport_type
1186         FROM message_transport_types
1187         ORDER BY message_transport_type
1188     ");
1189     return $mtts;
1190 }
1191
1192 =head2 GetMessage
1193
1194     my $message = C4::Letters::Message($message_id);
1195
1196 =cut
1197
1198 sub GetMessage {
1199     my ( $message_id ) = @_;
1200     return unless $message_id;
1201     my $dbh = C4::Context->dbh;
1202     return $dbh->selectrow_hashref(q|
1203         SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type
1204         FROM message_queue
1205         WHERE message_id = ?
1206     |, {}, $message_id );
1207 }
1208
1209 =head2 ResendMessage
1210
1211   Attempt to resend a message which has failed previously.
1212
1213   my $has_been_resent = C4::Letters::ResendMessage($message_id);
1214
1215   Updates the message to 'pending' status so that
1216   it will be resent later on.
1217
1218   returns 1 on success, 0 on failure, undef if no message was found
1219
1220 =cut
1221
1222 sub ResendMessage {
1223     my $message_id = shift;
1224     return unless $message_id;
1225
1226     my $message = GetMessage( $message_id );
1227     return unless $message;
1228     my $rv = 0;
1229     if ( $message->{status} ne 'pending' ) {
1230         $rv = C4::Letters::_set_message_status({
1231             message_id => $message_id,
1232             status => 'pending',
1233         });
1234         $rv = $rv > 0? 1: 0;
1235         # Clear destination email address to force address update
1236         _update_message_to_address( $message_id, undef ) if $rv &&
1237             $message->{message_transport_type} eq 'email';
1238     }
1239     return $rv;
1240 }
1241
1242 =head2 _add_attachements
1243
1244   named parameters:
1245   letter - the standard letter hashref
1246   attachments - listref of attachments. each attachment is a hashref of:
1247     type - the mime type, like 'text/plain'
1248     content - the actual attachment
1249     filename - the name of the attachment.
1250   message - a MIME::Lite object to attach these to.
1251
1252   returns your letter object, with the content updated.
1253
1254 =cut
1255
1256 sub _add_attachments {
1257     my $params = shift;
1258
1259     my $letter = $params->{'letter'};
1260     my $attachments = $params->{'attachments'};
1261     return $letter unless @$attachments;
1262     my $message = $params->{'message'};
1263
1264     # First, we have to put the body in as the first attachment
1265     $message->attach(
1266         Type => $letter->{'content-type'} || 'TEXT',
1267         Data => $letter->{'is_html'}
1268             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1269             : $letter->{'content'},
1270     );
1271
1272     foreach my $attachment ( @$attachments ) {
1273         $message->attach(
1274             Type     => $attachment->{'type'},
1275             Data     => $attachment->{'content'},
1276             Filename => $attachment->{'filename'},
1277         );
1278     }
1279     # we're forcing list context here to get the header, not the count back from grep.
1280     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1281     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1282     $letter->{'content'} = $message->body_as_string;
1283
1284     return $letter;
1285
1286 }
1287
1288 =head2 _get_unsent_messages
1289
1290   This function's parameter hash reference takes the following
1291   optional named parameters:
1292    message_transport_type: method of message sending (e.g. email, sms, etc.)
1293    borrowernumber        : who the message is to be sent
1294    letter_code           : type of message being sent (e.g. PASSWORD_RESET)
1295    limit                 : maximum number of messages to send
1296
1297   This function returns an array of matching hash referenced rows from
1298   message_queue with some borrower information added.
1299
1300 =cut
1301
1302 sub _get_unsent_messages {
1303     my $params = shift;
1304
1305     my $dbh = C4::Context->dbh();
1306     my $statement = qq{
1307         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
1308         FROM message_queue mq
1309         LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1310         WHERE status = ?
1311     };
1312
1313     my @query_params = ('pending');
1314     if ( ref $params ) {
1315         if ( $params->{'message_transport_type'} ) {
1316             $statement .= ' AND mq.message_transport_type = ? ';
1317             push @query_params, $params->{'message_transport_type'};
1318         }
1319         if ( $params->{'borrowernumber'} ) {
1320             $statement .= ' AND mq.borrowernumber = ? ';
1321             push @query_params, $params->{'borrowernumber'};
1322         }
1323         if ( $params->{'letter_code'} ) {
1324             $statement .= ' AND mq.letter_code = ? ';
1325             push @query_params, $params->{'letter_code'};
1326         }
1327         if ( $params->{'limit'} ) {
1328             $statement .= ' limit ? ';
1329             push @query_params, $params->{'limit'};
1330         }
1331     }
1332
1333     $debug and warn "_get_unsent_messages SQL: $statement";
1334     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1335     my $sth = $dbh->prepare( $statement );
1336     my $result = $sth->execute( @query_params );
1337     return $sth->fetchall_arrayref({});
1338 }
1339
1340 sub _send_message_by_email {
1341     my $message = shift or return;
1342     my ($username, $password, $method) = @_;
1343
1344     my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1345     my $to_address = $message->{'to_address'};
1346     unless ($to_address) {
1347         unless ($patron) {
1348             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1349             _set_message_status( { message_id => $message->{'message_id'},
1350                                    status     => 'failed' } );
1351             return;
1352         }
1353         $to_address = $patron->notice_email_address;
1354         unless ($to_address) {  
1355             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1356             # warning too verbose for this more common case?
1357             _set_message_status( { message_id => $message->{'message_id'},
1358                                    status     => 'failed' } );
1359             return;
1360         }
1361     }
1362
1363     my $utf8   = decode('MIME-Header', $message->{'subject'} );
1364     $message->{subject}= encode('MIME-Header', $utf8);
1365     my $subject = encode('UTF-8', $message->{'subject'});
1366     my $content = encode('UTF-8', $message->{'content'});
1367     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1368     my $is_html = $content_type =~ m/html/io;
1369     my $branch_email = undef;
1370     my $branch_replyto = undef;
1371     my $branch_returnpath = undef;
1372     if ($patron) {
1373         my $library = $patron->library;
1374         $branch_email      = $library->branchemail;
1375         $branch_replyto    = $library->branchreplyto;
1376         $branch_returnpath = $library->branchreturnpath;
1377     }
1378     my $email = Koha::Email->new();
1379     my %sendmail_params = $email->create_message_headers(
1380         {
1381             to      => $to_address,
1382             from    => $message->{'from_address'} || $branch_email,
1383             replyto => $branch_replyto,
1384             sender  => $branch_returnpath,
1385             subject => $subject,
1386             message => $is_html ? _wrap_html( $content, $subject ) : $content,
1387             contenttype => $content_type
1388         }
1389     );
1390
1391     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1392     if ( my $bcc = C4::Context->preference('NoticeBcc') ) {
1393        $sendmail_params{ Bcc } = $bcc;
1394     }
1395
1396     _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
1397
1398     if ( sendmail( %sendmail_params ) ) {
1399         _set_message_status( { message_id => $message->{'message_id'},
1400                 status     => 'sent' } );
1401         return 1;
1402     } else {
1403         _set_message_status( { message_id => $message->{'message_id'},
1404                 status     => 'failed' } );
1405         carp $Mail::Sendmail::error;
1406         return;
1407     }
1408 }
1409
1410 sub _wrap_html {
1411     my ($content, $title) = @_;
1412
1413     my $css = C4::Context->preference("NoticeCSS") || '';
1414     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1415     return <<EOS;
1416 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1417     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1418 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1419 <head>
1420 <title>$title</title>
1421 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1422 $css
1423 </head>
1424 <body>
1425 $content
1426 </body>
1427 </html>
1428 EOS
1429 }
1430
1431 sub _is_duplicate {
1432     my ( $message ) = @_;
1433     my $dbh = C4::Context->dbh;
1434     my $count = $dbh->selectrow_array(q|
1435         SELECT COUNT(*)
1436         FROM message_queue
1437         WHERE message_transport_type = ?
1438         AND borrowernumber = ?
1439         AND letter_code = ?
1440         AND CAST(time_queued AS date) = CAST(NOW() AS date)
1441         AND status="sent"
1442         AND content = ?
1443     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1444     return $count;
1445 }
1446
1447 sub _send_message_by_sms {
1448     my $message = shift or return;
1449     my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1450
1451     unless ( $patron and $patron->smsalertnumber ) {
1452         _set_message_status( { message_id => $message->{'message_id'},
1453                                status     => 'failed' } );
1454         return;
1455     }
1456
1457     if ( _is_duplicate( $message ) ) {
1458         _set_message_status( { message_id => $message->{'message_id'},
1459                                status     => 'failed' } );
1460         return;
1461     }
1462
1463     my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1464                                        message     => $message->{'content'},
1465                                      } );
1466     _set_message_status( { message_id => $message->{'message_id'},
1467                            status     => ($success ? 'sent' : 'failed') } );
1468     return $success;
1469 }
1470
1471 sub _update_message_to_address {
1472     my ($id, $to)= @_;
1473     my $dbh = C4::Context->dbh();
1474     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1475 }
1476
1477 sub _set_message_status {
1478     my $params = shift or return;
1479
1480     foreach my $required_parameter ( qw( message_id status ) ) {
1481         return unless exists $params->{ $required_parameter };
1482     }
1483
1484     my $dbh = C4::Context->dbh();
1485     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1486     my $sth = $dbh->prepare( $statement );
1487     my $result = $sth->execute( $params->{'status'},
1488                                 $params->{'message_id'} );
1489     return $result;
1490 }
1491
1492 sub _process_tt {
1493     my ( $params ) = @_;
1494
1495     my $content = $params->{content};
1496     my $tables = $params->{tables};
1497     my $loops = $params->{loops};
1498     my $substitute = $params->{substitute} || {};
1499
1500     my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1501     my $template           = Template->new(
1502         {
1503             EVAL_PERL    => 1,
1504             ABSOLUTE     => 1,
1505             PLUGIN_BASE  => 'Koha::Template::Plugin',
1506             COMPILE_EXT  => $use_template_cache ? '.ttc' : '',
1507             COMPILE_DIR  => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1508             FILTERS      => {},
1509             ENCODING     => 'UTF-8',
1510         }
1511     ) or die Template->error();
1512
1513     my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1514
1515     $content = qq|[% USE KohaDates %]$content|;
1516
1517     my $output;
1518     $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1519
1520     return $output;
1521 }
1522
1523 sub _get_tt_params {
1524     my ($tables, $is_a_loop) = @_;
1525
1526     my $params;
1527     $is_a_loop ||= 0;
1528
1529     my $config = {
1530         article_requests => {
1531             module   => 'Koha::ArticleRequests',
1532             singular => 'article_request',
1533             plural   => 'article_requests',
1534             pk       => 'id',
1535           },
1536         biblio => {
1537             module   => 'Koha::Biblios',
1538             singular => 'biblio',
1539             plural   => 'biblios',
1540             pk       => 'biblionumber',
1541         },
1542         borrowers => {
1543             module   => 'Koha::Patrons',
1544             singular => 'borrower',
1545             plural   => 'borrowers',
1546             pk       => 'borrowernumber',
1547         },
1548         branches => {
1549             module   => 'Koha::Libraries',
1550             singular => 'branch',
1551             plural   => 'branches',
1552             pk       => 'branchcode',
1553         },
1554         items => {
1555             module   => 'Koha::Items',
1556             singular => 'item',
1557             plural   => 'items',
1558             pk       => 'itemnumber',
1559         },
1560         opac_news => {
1561             module   => 'Koha::News',
1562             singular => 'news',
1563             plural   => 'news',
1564             pk       => 'idnew',
1565         },
1566         aqorders => {
1567             module   => 'Koha::Acquisition::Orders',
1568             singular => 'order',
1569             plural   => 'orders',
1570             pk       => 'ordernumber',
1571         },
1572         reserves => {
1573             module   => 'Koha::Holds',
1574             singular => 'hold',
1575             plural   => 'holds',
1576             fk       => [ 'borrowernumber', 'biblionumber' ],
1577         },
1578         serial => {
1579             module   => 'Koha::Serials',
1580             singular => 'serial',
1581             plural   => 'serials',
1582             pk       => 'serialid',
1583         },
1584         subscription => {
1585             module   => 'Koha::Subscriptions',
1586             singular => 'subscription',
1587             plural   => 'subscriptions',
1588             pk       => 'subscriptionid',
1589         },
1590         suggestions => {
1591             module   => 'Koha::Suggestions',
1592             singular => 'suggestion',
1593             plural   => 'suggestions',
1594             pk       => 'suggestionid',
1595         },
1596         issues => {
1597             module   => 'Koha::Checkouts',
1598             singular => 'checkout',
1599             plural   => 'checkouts',
1600             fk       => 'itemnumber',
1601         },
1602         old_issues => {
1603             module   => 'Koha::Old::Checkouts',
1604             singular => 'old_checkout',
1605             plural   => 'old_checkouts',
1606             fk       => 'itemnumber',
1607         },
1608         overdues => {
1609             module   => 'Koha::Checkouts',
1610             singular => 'overdue',
1611             plural   => 'overdues',
1612             fk       => 'itemnumber',
1613         },
1614         borrower_modifications => {
1615             module   => 'Koha::Patron::Modifications',
1616             singular => 'patron_modification',
1617             plural   => 'patron_modifications',
1618             fk       => 'verification_token',
1619         },
1620     };
1621
1622     foreach my $table ( keys %$tables ) {
1623         next unless $config->{$table};
1624
1625         my $ref = ref( $tables->{$table} ) || q{};
1626         my $module = $config->{$table}->{module};
1627
1628         if ( can_load( modules => { $module => undef } ) ) {
1629             my $pk = $config->{$table}->{pk};
1630             my $fk = $config->{$table}->{fk};
1631
1632             if ( $is_a_loop ) {
1633                 my $values = $tables->{$table} || [];
1634                 unless ( ref( $values ) eq 'ARRAY' ) {
1635                     croak "ERROR processing table $table. Wrong API call.";
1636                 }
1637                 my $key = $pk ? $pk : $fk;
1638                 # $key does not come from user input
1639                 my $objects = $module->search(
1640                     { $key => $values },
1641                     {
1642                             # We want to retrieve the data in the same order
1643                             # FIXME MySQLism
1644                             # field is a MySQLism, but they are no other way to do it
1645                             # To be generic we could do it in perl, but we will need to fetch
1646                             # all the data then order them
1647                         @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1648                     }
1649                 );
1650                 $params->{ $config->{$table}->{plural} } = $objects;
1651             }
1652             elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1653                 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1654                 my $object;
1655                 if ( $fk ) { # Using a foreign key for lookup
1656                     if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1657                         my $search;
1658                         foreach my $key ( @$fk ) {
1659                             $search->{$key} = $id->{$key};
1660                         }
1661                         $object = $module->search( $search )->last();
1662                     } else { # Foreign key is single column
1663                         $object = $module->search( { $fk => $id } )->last();
1664                     }
1665                 } else { # using the table's primary key for lookup
1666                     $object = $module->find($id);
1667                 }
1668                 $params->{ $config->{$table}->{singular} } = $object;
1669             }
1670             else {    # $ref eq 'ARRAY'
1671                 my $object;
1672                 if ( @{ $tables->{$table} } == 1 ) {    # Param is a single key
1673                     $object = $module->search( { $pk => $tables->{$table} } )->last();
1674                 }
1675                 else {                                  # Params are mutliple foreign keys
1676                     croak "Multiple foreign keys (table $table) should be passed using an hashref";
1677                 }
1678                 $params->{ $config->{$table}->{singular} } = $object;
1679             }
1680         }
1681         else {
1682             croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1683         }
1684     }
1685
1686     $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1687
1688     return $params;
1689 }
1690
1691 =head2 get_item_content
1692
1693     my $item = Koha::Items->find(...)->unblessed;
1694     my @item_content_fields = qw( date_due title barcode author itemnumber );
1695     my $item_content = C4::Letters::get_item_content({
1696                              item => $item,
1697                              item_content_fields => \@item_content_fields
1698                        });
1699
1700 This function generates a tab-separated list of values for the passed item. Dates
1701 are formatted following the current setup.
1702
1703 =cut
1704
1705 sub get_item_content {
1706     my ( $params ) = @_;
1707     my $item = $params->{item};
1708     my $dateonly = $params->{dateonly} || 0;
1709     my $item_content_fields = $params->{item_content_fields} || [];
1710
1711     return unless $item;
1712
1713     my @item_info = map {
1714         $_ =~ /^date|date$/
1715           ? eval {
1716             output_pref(
1717                 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1718           }
1719           : $item->{$_}
1720           || ''
1721     } @$item_content_fields;
1722     return join( "\t", @item_info ) . "\n";
1723 }
1724
1725 1;
1726 __END__