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