Bug 12357: Fix comma issues related to additional fields.
[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 strict;
21 use warnings;
22
23 use MIME::Lite;
24 use Mail::Sendmail;
25
26 use C4::Koha qw(GetAuthorisedValueByCode);
27 use C4::Members;
28 use C4::Members::Attributes qw(GetBorrowerAttributes);
29 use C4::Branch;
30 use C4::Log;
31 use C4::SMS;
32 use C4::Debug;
33 use Koha::DateUtils;
34 use Date::Calc qw( Add_Delta_Days );
35 use Encode;
36 use Carp;
37 use Koha::Email;
38
39 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
40
41 BEGIN {
42     require Exporter;
43     # set the version for version checking
44     $VERSION = 3.07.00.049;
45     @ISA = qw(Exporter);
46     @EXPORT = qw(
47         &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &addalert &getalert &delalert &findrelatedto &SendAlerts &GetPrintMessages &GetMessageTransportTypes
48     );
49 }
50
51 =head1 NAME
52
53 C4::Letters - Give functions for Letters management
54
55 =head1 SYNOPSIS
56
57   use C4::Letters;
58
59 =head1 DESCRIPTION
60
61   "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
62   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)
63
64   Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
65
66 =head2 GetLetters([$module])
67
68   $letters = &GetLetters($module);
69   returns informations about letters.
70   if needed, $module filters for letters given module
71
72 =cut
73
74 sub GetLetters {
75     my ($filters) = @_;
76     my $module    = $filters->{module};
77     my $code      = $filters->{code};
78     my $branchcode = $filters->{branchcode};
79     my $dbh       = C4::Context->dbh;
80     my $letters   = $dbh->selectall_arrayref(
81         q|
82             SELECT module, code, branchcode, name
83             FROM letter
84             WHERE 1
85         |
86           . ( $module ? q| AND module = ?| : q|| )
87           . ( $code   ? q| AND code = ?|   : q|| )
88           . ( defined $branchcode   ? q| AND branchcode = ?|   : q|| )
89           . q| GROUP BY code ORDER BY name|, { Slice => {} }
90         , ( $module ? $module : () )
91         , ( $code ? $code : () )
92         , ( defined $branchcode ? $branchcode : () )
93     );
94
95     return $letters;
96 }
97
98 =head2 GetLetterTemplates
99
100     my $letter_templates = GetLetterTemplates(
101         {
102             module => 'circulation',
103             code => 'my code',
104             branchcode => 'CPL', # '' for default,
105         }
106     );
107
108     Return a hashref of letter templates.
109     The key will be the message transport type.
110
111 =cut
112
113 sub GetLetterTemplates {
114     my ( $params ) = @_;
115
116     my $module    = $params->{module};
117     my $code      = $params->{code};
118     my $branchcode = $params->{branchcode} // '';
119     my $dbh       = C4::Context->dbh;
120     my $letters   = $dbh->selectall_hashref(
121         q|
122             SELECT module, code, branchcode, name, is_html, title, content, message_transport_type
123             FROM letter
124             WHERE module = ?
125             AND code = ?
126             and branchcode = ?
127         |
128         , 'message_transport_type'
129         , undef
130         , $module, $code, $branchcode
131     );
132
133     return $letters;
134 }
135
136 =head2 GetLettersAvailableForALibrary
137
138     my $letters = GetLettersAvailableForALibrary(
139         {
140             branchcode => 'CPL', # '' for default
141             module => 'circulation',
142         }
143     );
144
145     Return an arrayref of letters, sorted by name.
146     If a specific letter exist for the given branchcode, it will be retrieve.
147     Otherwise the default letter will be.
148
149 =cut
150
151 sub GetLettersAvailableForALibrary {
152     my ($filters)  = @_;
153     my $branchcode = $filters->{branchcode};
154     my $module     = $filters->{module};
155
156     croak "module should be provided" unless $module;
157
158     my $dbh             = C4::Context->dbh;
159     my $default_letters = $dbh->selectall_arrayref(
160         q|
161             SELECT module, code, branchcode, name
162             FROM letter
163             WHERE 1
164         |
165           . q| AND branchcode = ''|
166           . ( $module ? q| AND module = ?| : q|| )
167           . q| ORDER BY name|, { Slice => {} }
168         , ( $module ? $module : () )
169     );
170
171     my $specific_letters;
172     if ($branchcode) {
173         $specific_letters = $dbh->selectall_arrayref(
174             q|
175                 SELECT module, code, branchcode, name
176                 FROM letter
177                 WHERE 1
178             |
179               . q| AND branchcode = ?|
180               . ( $module ? q| AND module = ?| : q|| )
181               . q| ORDER BY name|, { Slice => {} }
182             , $branchcode
183             , ( $module ? $module : () )
184         );
185     }
186
187     my %letters;
188     for my $l (@$default_letters) {
189         $letters{ $l->{code} } = $l;
190     }
191     for my $l (@$specific_letters) {
192         # Overwrite the default letter with the specific one.
193         $letters{ $l->{code} } = $l;
194     }
195
196     return [ map { $letters{$_} }
197           sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
198           keys %letters ];
199
200 }
201
202 # FIXME: using our here means that a Plack server will need to be
203 #        restarted fairly regularly when working with this routine.
204 #        A better option would be to use Koha::Cache and use a cache
205 #        that actually works in a persistent environment, but as a
206 #        short-term fix, our will work.
207 our %letter;
208 sub getletter {
209     my ( $module, $code, $branchcode, $message_transport_type ) = @_;
210     $message_transport_type //= '%';
211
212     if ( C4::Context->preference('IndependentBranches')
213             and $branchcode
214             and C4::Context->userenv ) {
215
216         $branchcode = C4::Context->userenv->{'branch'};
217     }
218     $branchcode //= '';
219
220     if ( my $l = $letter{$module}{$code}{$branchcode}{$message_transport_type} ) {
221         return { %$l }; # deep copy
222     }
223
224     my $dbh = C4::Context->dbh;
225     my $sth = $dbh->prepare(q{
226         SELECT *
227         FROM letter
228         WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
229         AND message_transport_type LIKE ?
230         ORDER BY branchcode DESC LIMIT 1
231     });
232     $sth->execute( $module, $code, $branchcode, $message_transport_type );
233     my $line = $sth->fetchrow_hashref
234       or return;
235     $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
236     $letter{$module}{$code}{$branchcode}{$message_transport_type} = $line;
237     return { %$line };
238 }
239
240
241 =head2 DelLetter
242
243     DelLetter(
244         {
245             branchcode => 'CPL',
246             module => 'circulation',
247             code => 'my code',
248             [ mtt => 'email', ]
249         }
250     );
251
252     Delete the letter. The mtt parameter is facultative.
253     If not given, all templates mathing the other parameters will be removed.
254
255 =cut
256
257 sub DelLetter {
258     my ($params)   = @_;
259     my $branchcode = $params->{branchcode};
260     my $module     = $params->{module};
261     my $code       = $params->{code};
262     my $mtt        = $params->{mtt};
263     my $dbh        = C4::Context->dbh;
264     $dbh->do(q|
265         DELETE FROM letter
266         WHERE branchcode = ?
267           AND module = ?
268           AND code = ?
269     | . ( $mtt ? q| AND message_transport_type = ?| : q|| )
270     , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ) );
271 }
272
273 =head2 addalert ($borrowernumber, $type, $externalid)
274
275     parameters : 
276     - $borrowernumber : the number of the borrower subscribing to the alert
277     - $type : the type of alert.
278     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
279     
280     create an alert and return the alertid (primary key)
281
282 =cut
283
284 sub addalert {
285     my ( $borrowernumber, $type, $externalid ) = @_;
286     my $dbh = C4::Context->dbh;
287     my $sth =
288       $dbh->prepare(
289         "insert into alert (borrowernumber, type, externalid) values (?,?,?)");
290     $sth->execute( $borrowernumber, $type, $externalid );
291
292     # get the alert number newly created and return it
293     my $alertid = $dbh->{'mysql_insertid'};
294     return $alertid;
295 }
296
297 =head2 delalert ($alertid)
298
299     parameters :
300     - alertid : the alert id
301     deletes the alert
302
303 =cut
304
305 sub delalert {
306     my $alertid = shift or die "delalert() called without valid argument (alertid)";    # it's gonna die anyway.
307     $debug and warn "delalert: deleting alertid $alertid";
308     my $sth = C4::Context->dbh->prepare("delete from alert where alertid=?");
309     $sth->execute($alertid);
310 }
311
312 =head2 getalert ([$borrowernumber], [$type], [$externalid])
313
314     parameters :
315     - $borrowernumber : the number of the borrower subscribing to the alert
316     - $type : the type of alert.
317     - $externalid : the primary key of the object to put alert on. For issues, the alert is made on subscriptionid.
318     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.
319
320 =cut
321
322 sub getalert {
323     my ( $borrowernumber, $type, $externalid ) = @_;
324     my $dbh   = C4::Context->dbh;
325     my $query = "SELECT a.*, b.branchcode FROM alert a JOIN borrowers b USING(borrowernumber) WHERE";
326     my @bind;
327     if ($borrowernumber and $borrowernumber =~ /^\d+$/) {
328         $query .= " borrowernumber=? AND ";
329         push @bind, $borrowernumber;
330     }
331     if ($type) {
332         $query .= " type=? AND ";
333         push @bind, $type;
334     }
335     if ($externalid) {
336         $query .= " externalid=? AND ";
337         push @bind, $externalid;
338     }
339     $query =~ s/ AND $//;
340     my $sth = $dbh->prepare($query);
341     $sth->execute(@bind);
342     return $sth->fetchall_arrayref({});
343 }
344
345 =head2 findrelatedto($type, $externalid)
346
347     parameters :
348     - $type : the type of alert
349     - $externalid : the id of the "object" to query
350
351     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.
352     When type=issue, the id is related to a subscriptionid and this sub returns the name of the biblio.
353
354 =cut
355     
356 # outmoded POD:
357 # When type=virtual, the id is related to a virtual shelf and this sub returns the name of the sub
358
359 sub findrelatedto {
360     my $type       = shift or return;
361     my $externalid = shift or return;
362     my $q = ($type eq 'issue'   ) ?
363 "select title as result from subscription left join biblio on subscription.biblionumber=biblio.biblionumber where subscriptionid=?" :
364             ($type eq 'borrower') ?
365 "select concat(firstname,' ',surname) from borrowers where borrowernumber=?" : undef;
366     unless ($q) {
367         warn "findrelatedto(): Illegal type '$type'";
368         return;
369     }
370     my $sth = C4::Context->dbh->prepare($q);
371     $sth->execute($externalid);
372     my ($result) = $sth->fetchrow;
373     return $result;
374 }
375
376 =head2 SendAlerts
377
378     parameters :
379     - $type : the type of alert
380     - $externalid : the id of the "object" to query
381     - $letter_code : the letter to send.
382
383     send an alert to all borrowers having put an alert on a given subject.
384
385 =cut
386
387 sub SendAlerts {
388     my ( $type, $externalid, $letter_code ) = @_;
389     my $dbh = C4::Context->dbh;
390     if ( $type eq 'issue' ) {
391
392         # prepare the letter...
393         # search the subscriptionid
394         my $sth =
395           $dbh->prepare(
396             "SELECT subscriptionid FROM serial WHERE serialid=?");
397         $sth->execute($externalid);
398         my ($subscriptionid) = $sth->fetchrow
399           or warn( "No subscription for '$externalid'" ),
400              return;
401
402         # search the biblionumber
403         $sth =
404           $dbh->prepare(
405             "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
406         $sth->execute($subscriptionid);
407         my ($biblionumber) = $sth->fetchrow
408           or warn( "No biblionumber for '$subscriptionid'" ),
409              return;
410
411         my %letter;
412         # find the list of borrowers to alert
413         my $alerts = getalert( '', 'issue', $subscriptionid );
414         foreach (@$alerts) {
415             my $borinfo = C4::Members::GetMember('borrowernumber' => $_->{'borrowernumber'});
416             my $email = $borinfo->{email} or next;
417
418 #                    warn "sending issues...";
419             my $userenv = C4::Context->userenv;
420             my $branchdetails = GetBranchDetail($_->{'branchcode'});
421             my $letter = GetPreparedLetter (
422                 module => 'serial',
423                 letter_code => $letter_code,
424                 branchcode => $userenv->{branch},
425                 tables => {
426                     'branches'    => $_->{branchcode},
427                     'biblio'      => $biblionumber,
428                     'biblioitems' => $biblionumber,
429                     'borrowers'   => $borinfo,
430                     'subscription' => $subscriptionid,
431                     'serial' => $externalid,
432                 },
433                 want_librarian => 1,
434             ) or return;
435
436             # ... then send mail
437             my $message = Koha::Email->new();
438             my %mail = $message->create_message_headers(
439                 {
440                     to      => $email,
441                     from    => $branchdetails->{'branchemail'},
442                     replyto => $branchdetails->{'branchreplyto'},
443                     sender  => $branchdetails->{'branchreturnpath'},
444                     subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
445                     message => $letter->{'is_html'}
446                                 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
447                                               Encode::encode( "UTF-8", "" . $letter->{'title'} ))
448                                 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
449                     contenttype => $letter->{'is_html'}
450                                     ? 'text/html; charset="utf-8"'
451                                     : 'text/plain; charset="utf-8"',
452                 }
453             );
454             sendmail(%mail) or carp $Mail::Sendmail::error;
455         }
456     }
457     elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' ) {
458
459         # prepare the letter...
460         # search the biblionumber
461         my $strsth =  $type eq 'claimacquisition'
462             ? qq{
463             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
464             FROM aqorders
465             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
466             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
467             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
468             WHERE aqorders.ordernumber IN (
469             }
470             : qq{
471             SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
472             aqbooksellers.id AS booksellerid
473             FROM serial
474             LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
475             LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
476             LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
477             WHERE serial.serialid IN (
478             };
479
480         if (!@$externalid){
481             carp "No Order seleted";
482             return { error => "no_order_seleted" };
483         }
484
485         $strsth .= join( ",", @$externalid ) . ")";
486         my $sthorders = $dbh->prepare($strsth);
487         $sthorders->execute;
488         my $dataorders = $sthorders->fetchall_arrayref( {} );
489
490         my $sthbookseller =
491           $dbh->prepare("select * from aqbooksellers where id=?");
492         $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
493         my $databookseller = $sthbookseller->fetchrow_hashref;
494         my $addressee =  $type eq 'claimacquisition' ? 'acqprimary' : 'serialsprimary';
495         my $sthcontact =
496           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
497         $sthcontact->execute( $dataorders->[0]->{booksellerid} );
498         my $datacontact = $sthcontact->fetchrow_hashref;
499
500         my @email;
501         my @cc;
502         push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
503         push @email, $datacontact->{email}           if ( $datacontact && $datacontact->{email} );
504         unless (@email) {
505             warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
506             return { error => "no_email" };
507         }
508         my $addlcontact;
509         while ($addlcontact = $sthcontact->fetchrow_hashref) {
510             push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
511         }
512
513         my $userenv = C4::Context->userenv;
514         my $letter = GetPreparedLetter (
515             module => $type,
516             letter_code => $letter_code,
517             branchcode => $userenv->{branch},
518             tables => {
519                 'branches'    => $userenv->{branch},
520                 'aqbooksellers' => $databookseller,
521                 'aqcontacts'    => $datacontact,
522             },
523             repeat => $dataorders,
524             want_librarian => 1,
525         ) or return;
526
527         # Remove the order tag
528         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
529
530         # ... then send mail
531         my %mail = (
532             To => join( ',', @email),
533             Cc             => join( ',', @cc),
534             From           => $userenv->{emailaddress},
535             Subject        => Encode::encode( "UTF-8", "" . $letter->{title} ),
536             Message => $letter->{'is_html'}
537                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
538                                           Encode::encode( "UTF-8", "" . $letter->{'title'} ))
539                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
540             'Content-Type' => $letter->{'is_html'}
541                                 ? 'text/html; charset="utf-8"'
542                                 : 'text/plain; charset="utf-8"',
543         );
544
545         $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
546           if C4::Context->preference('ReplytoDefault');
547         $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
548           if C4::Context->preference('ReturnpathDefault');
549
550         unless ( sendmail(%mail) ) {
551             carp $Mail::Sendmail::error;
552             return { error => $Mail::Sendmail::error };
553         }
554
555         logaction(
556             "ACQUISITION",
557             $type eq 'claimissues' ? "CLAIM ISSUE" : "ACQUISITION CLAIM",
558             undef,
559             "To="
560                 . join( ',', @email )
561                 . " Title="
562                 . $letter->{title}
563                 . " Content="
564                 . $letter->{content}
565         ) if C4::Context->preference("LetterLog");
566     }
567    # send an "account details" notice to a newly created user
568     elsif ( $type eq 'members' ) {
569         my $branchdetails = GetBranchDetail($externalid->{'branchcode'});
570         my $letter = GetPreparedLetter (
571             module => 'members',
572             letter_code => $letter_code,
573             branchcode => $externalid->{'branchcode'},
574             tables => {
575                 'branches'    => $branchdetails,
576                 'borrowers' => $externalid->{'borrowernumber'},
577             },
578             substitute => { 'borrowers.password' => $externalid->{'password'} },
579             want_librarian => 1,
580         ) or return;
581         return { error => "no_email" } unless $externalid->{'emailaddr'};
582         my $email = Koha::Email->new();
583         my %mail  = $email->create_message_headers(
584             {
585                 to      => $externalid->{'emailaddr'},
586                 from    => $branchdetails->{'branchemail'},
587                 replyto => $branchdetails->{'branchreplyto'},
588                 sender  => $branchdetails->{'branchreturnpath'},
589                 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
590                 message => $letter->{'is_html'}
591                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
592                                           Encode::encode( "UTF-8", "" . $letter->{'title'}  ) )
593                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
594                 contenttype => $letter->{'is_html'}
595                                 ? 'text/html; charset="utf-8"'
596                                 : 'text/plain; charset="utf-8"',
597             }
598         );
599         sendmail(%mail) or carp $Mail::Sendmail::error;
600     }
601 }
602
603 =head2 GetPreparedLetter( %params )
604
605     %params hash:
606       module => letter module, mandatory
607       letter_code => letter code, mandatory
608       branchcode => for letter selection, if missing default system letter taken
609       tables => a hashref with table names as keys. Values are either:
610         - a scalar - primary key value
611         - an arrayref - primary key values
612         - a hashref - full record
613       substitute => custom substitution key/value pairs
614       repeat => records to be substituted on consecutive lines:
615         - an arrayref - tries to guess what needs substituting by
616           taking remaining << >> tokensr; not recommended
617         - a hashref token => @tables - replaces <token> << >> << >> </token>
618           subtemplate for each @tables row; table is a hashref as above
619       want_librarian => boolean,  if set to true triggers librarian details
620         substitution from the userenv
621     Return value:
622       letter fields hashref (title & content useful)
623
624 =cut
625
626 sub GetPreparedLetter {
627     my %params = @_;
628
629     my $module      = $params{module} or croak "No module";
630     my $letter_code = $params{letter_code} or croak "No letter_code";
631     my $branchcode  = $params{branchcode} || '';
632     my $mtt         = $params{message_transport_type} || 'email';
633
634     my $letter = getletter( $module, $letter_code, $branchcode, $mtt )
635         or warn( "No $module $letter_code letter transported by " . $mtt ),
636             return;
637
638     my $tables = $params{tables};
639     my $substitute = $params{substitute};
640     my $repeat = $params{repeat};
641     $tables || $substitute || $repeat
642       or carp( "ERROR: nothing to substitute - both 'tables' and 'substitute' are empty" ),
643          return;
644     my $want_librarian = $params{want_librarian};
645
646     if ($substitute) {
647         while ( my ($token, $val) = each %$substitute ) {
648             if ( $token eq 'items.content' ) {
649                 $val =~ s|\n|<br/>|g if $letter->{is_html};
650             }
651
652             $letter->{title} =~ s/<<$token>>/$val/g;
653             $letter->{content} =~ s/<<$token>>/$val/g;
654        }
655     }
656
657     my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
658     $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
659
660     if ($want_librarian) {
661         # parsing librarian name
662         my $userenv = C4::Context->userenv;
663         $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
664         $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
665         $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
666     }
667
668     my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
669
670     if ($repeat) {
671         if (ref ($repeat) eq 'ARRAY' ) {
672             $repeat_no_enclosing_tags = $repeat;
673         } else {
674             $repeat_enclosing_tags = $repeat;
675         }
676     }
677
678     if ($repeat_enclosing_tags) {
679         while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
680             if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
681                 my $subcontent = $1;
682                 my @lines = map {
683                     my %subletter = ( title => '', content => $subcontent );
684                     _substitute_tables( \%subletter, $_ );
685                     $subletter{content};
686                 } @$tag_tables;
687                 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
688             }
689         }
690     }
691
692     if ($tables) {
693         _substitute_tables( $letter, $tables );
694     }
695
696     if ($repeat_no_enclosing_tags) {
697         if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
698             my $line = $&;
699             my $i = 1;
700             my @lines = map {
701                 my $c = $line;
702                 $c =~ s/<<count>>/$i/go;
703                 foreach my $field ( keys %{$_} ) {
704                     $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
705                 }
706                 $i++;
707                 $c;
708             } @$repeat_no_enclosing_tags;
709
710             my $replaceby = join( "\n", @lines );
711             $letter->{content} =~ s/\Q$line\E/$replaceby/s;
712         }
713     }
714
715     $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
716 #   $letter->{content} =~ s/<<[^>]*>>//go;
717
718     return $letter;
719 }
720
721 sub _substitute_tables {
722     my ( $letter, $tables ) = @_;
723     while ( my ($table, $param) = each %$tables ) {
724         next unless $param;
725
726         my $ref = ref $param;
727
728         my $values;
729         if ($ref && $ref eq 'HASH') {
730             $values = $param;
731         }
732         else {
733             my $sth = _parseletter_sth($table);
734             unless ($sth) {
735                 warn "_parseletter_sth('$table') failed to return a valid sth.  No substitution will be done for that table.";
736                 return;
737             }
738             $sth->execute( $ref ? @$param : $param );
739
740             $values = $sth->fetchrow_hashref;
741             $sth->finish();
742         }
743
744         _parseletter ( $letter, $table, $values );
745     }
746 }
747
748 sub _parseletter_sth {
749     my $table = shift;
750     my $sth;
751     unless ($table) {
752         carp "ERROR: _parseletter_sth() called without argument (table)";
753         return;
754     }
755     # NOTE: we used to check whether we had a statement handle cached in
756     #       a %handles module-level variable. This was a dumb move and
757     #       broke things for the rest of us. prepare_cached is a better
758     #       way to cache statement handles anyway.
759     my $query = 
760     ($table eq 'biblio'       ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
761     ($table eq 'biblioitems'  ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
762     ($table eq 'items'        ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
763     ($table eq 'issues'       ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
764     ($table eq 'old_issues'   ) ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
765     ($table eq 'reserves'     ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
766     ($table eq 'borrowers'    ) ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
767     ($table eq 'branches'     ) ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
768     ($table eq 'suggestions'  ) ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
769     ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE             id = ?"                                  :
770     ($table eq 'aqorders'     ) ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
771     ($table eq 'opac_news'    ) ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
772     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
773     ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
774     ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
775     undef ;
776     unless ($query) {
777         warn "ERROR: No _parseletter_sth query for table '$table'";
778         return;     # nothing to get
779     }
780     unless ($sth = C4::Context->dbh->prepare_cached($query)) {
781         warn "ERROR: Failed to prepare query: '$query'";
782         return;
783     }
784     return $sth;    # now cache is populated for that $table
785 }
786
787 =head2 _parseletter($letter, $table, $values)
788
789     parameters :
790     - $letter : a hash to letter fields (title & content useful)
791     - $table : the Koha table to parse.
792     - $values : table record hashref
793     parse all fields from a table, and replace values in title & content with the appropriate value
794     (not exported sub, used only internally)
795
796 =cut
797
798 sub _parseletter {
799     my ( $letter, $table, $values ) = @_;
800
801     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
802         my @waitingdate = split /-/, $values->{'waitingdate'};
803
804         $values->{'expirationdate'} = '';
805         if( C4::Context->preference('ExpireReservesMaxPickUpDelay') &&
806         C4::Context->preference('ReservesMaxPickUpDelay') ) {
807             my $dt = dt_from_string();
808             $dt->add( days => C4::Context->preference('ReservesMaxPickUpDelay') );
809             $values->{'expirationdate'} = output_pref({ dt => $dt, dateonly => 1 });
810         }
811
812         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
813
814     }
815
816     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
817         my $todaysdate = output_pref( DateTime->now() );
818         $letter->{content} =~ s/<<today>>/$todaysdate/go;
819     }
820
821     while ( my ($field, $val) = each %$values ) {
822         my $replacetablefield = "<<$table.$field>>";
823         my $replacefield = "<<$field>>";
824         $val =~ s/\p{P}$// if $val && $table=~/biblio/;
825             #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
826             #Therefore adding the test on biblio. This includes biblioitems,
827             #but excludes items. Removed unneeded global and lookahead.
828
829         $val = GetAuthorisedValueByCode ('ROADTYPE', $val, 0) if $table=~/^borrowers$/ && $field=~/^streettype$/;
830         my $replacedby   = defined ($val) ? $val : '';
831         if (    $replacedby
832             and not $replacedby =~ m|0000-00-00|
833             and not $replacedby =~ m|9999-12-31|
834             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
835         {
836             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
837             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
838             eval {
839                 $replacedby = output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
840             };
841             warn "$replacedby seems to be a date but an error occurs on generating it ($@)" if $@;
842         }
843         ($letter->{title}  ) and do {
844             $letter->{title}   =~ s/$replacetablefield/$replacedby/g;
845             $letter->{title}   =~ s/$replacefield/$replacedby/g;
846         };
847         ($letter->{content}) and do {
848             $letter->{content} =~ s/$replacetablefield/$replacedby/g;
849             $letter->{content} =~ s/$replacefield/$replacedby/g;
850         };
851     }
852
853     if ($table eq 'borrowers' && $letter->{content}) {
854         if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
855             my %attr;
856             foreach (@$attributes) {
857                 my $code = $_->{code};
858                 my $val  = $_->{value_description} || $_->{value};
859                 $val =~ s/\p{P}(?=$)//g if $val;
860                 next unless $val gt '';
861                 $attr{$code} ||= [];
862                 push @{ $attr{$code} }, $val;
863             }
864             while ( my ($code, $val_ar) = each %attr ) {
865                 my $replacefield = "<<borrower-attribute:$code>>";
866                 my $replacedby   = join ',', @$val_ar;
867                 $letter->{content} =~ s/$replacefield/$replacedby/g;
868             }
869         }
870     }
871     return $letter;
872 }
873
874 =head2 EnqueueLetter
875
876   my $success = EnqueueLetter( { letter => $letter, 
877         borrowernumber => '12', message_transport_type => 'email' } )
878
879 places a letter in the message_queue database table, which will
880 eventually get processed (sent) by the process_message_queue.pl
881 cronjob when it calls SendQueuedMessages.
882
883 return message_id on success
884
885 =cut
886
887 sub EnqueueLetter {
888     my $params = shift or return;
889
890     return unless exists $params->{'letter'};
891 #   return unless exists $params->{'borrowernumber'};
892     return unless exists $params->{'message_transport_type'};
893
894     my $content = $params->{letter}->{content};
895     $content =~ s/\s+//g if(defined $content);
896     if ( not defined $content or $content eq '' ) {
897         warn "Trying to add an empty message to the message queue" if $debug;
898         return;
899     }
900
901     # If we have any attachments we should encode then into the body.
902     if ( $params->{'attachments'} ) {
903         $params->{'letter'} = _add_attachments(
904             {   letter      => $params->{'letter'},
905                 attachments => $params->{'attachments'},
906                 message     => MIME::Lite->new( Type => 'multipart/mixed' ),
907             }
908         );
909     }
910
911     my $dbh       = C4::Context->dbh();
912     my $statement = << 'ENDSQL';
913 INSERT INTO message_queue
914 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
915 VALUES
916 ( ?,              ?,       ?,       ?,        ?,           ?,                      ?,      NOW(),       ?,          ?,            ? )
917 ENDSQL
918
919     my $sth    = $dbh->prepare($statement);
920     my $result = $sth->execute(
921         $params->{'borrowernumber'},              # borrowernumber
922         $params->{'letter'}->{'title'},           # subject
923         $params->{'letter'}->{'content'},         # content
924         $params->{'letter'}->{'metadata'} || '',  # metadata
925         $params->{'letter'}->{'code'}     || '',  # letter_code
926         $params->{'message_transport_type'},      # message_transport_type
927         'pending',                                # status
928         $params->{'to_address'},                  # to_address
929         $params->{'from_address'},                # from_address
930         $params->{'letter'}->{'content-type'},    # content_type
931     );
932     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
933 }
934
935 =head2 SendQueuedMessages ([$hashref]) 
936
937   my $sent = SendQueuedMessages( { verbose => 1 } );
938
939 sends all of the 'pending' items in the message queue.
940
941 returns number of messages sent.
942
943 =cut
944
945 sub SendQueuedMessages {
946     my $params = shift;
947
948     my $unsent_messages = _get_unsent_messages();
949     MESSAGE: foreach my $message ( @$unsent_messages ) {
950         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
951         warn sprintf( 'sending %s message to patron: %s',
952                       $message->{'message_transport_type'},
953                       $message->{'borrowernumber'} || 'Admin' )
954           if $params->{'verbose'} or $debug;
955         # This is just begging for subclassing
956         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
957         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
958             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
959         }
960         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
961             _send_message_by_sms( $message );
962         }
963     }
964     return scalar( @$unsent_messages );
965 }
966
967 =head2 GetRSSMessages
968
969   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
970
971 returns a listref of all queued RSS messages for a particular person.
972
973 =cut
974
975 sub GetRSSMessages {
976     my $params = shift;
977
978     return unless $params;
979     return unless ref $params;
980     return unless $params->{'borrowernumber'};
981     
982     return _get_unsent_messages( { message_transport_type => 'rss',
983                                    limit                  => $params->{'limit'},
984                                    borrowernumber         => $params->{'borrowernumber'}, } );
985 }
986
987 =head2 GetPrintMessages
988
989   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
990
991 Returns a arrayref of all queued print messages (optionally, for a particular
992 person).
993
994 =cut
995
996 sub GetPrintMessages {
997     my $params = shift || {};
998     
999     return _get_unsent_messages( { message_transport_type => 'print',
1000                                    borrowernumber         => $params->{'borrowernumber'},
1001                                  } );
1002 }
1003
1004 =head2 GetQueuedMessages ([$hashref])
1005
1006   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1007
1008 fetches messages out of the message queue.
1009
1010 returns:
1011 list of hashes, each has represents a message in the message queue.
1012
1013 =cut
1014
1015 sub GetQueuedMessages {
1016     my $params = shift;
1017
1018     my $dbh = C4::Context->dbh();
1019     my $statement = << 'ENDSQL';
1020 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1021 FROM message_queue
1022 ENDSQL
1023
1024     my @query_params;
1025     my @whereclauses;
1026     if ( exists $params->{'borrowernumber'} ) {
1027         push @whereclauses, ' borrowernumber = ? ';
1028         push @query_params, $params->{'borrowernumber'};
1029     }
1030
1031     if ( @whereclauses ) {
1032         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1033     }
1034
1035     if ( defined $params->{'limit'} ) {
1036         $statement .= ' LIMIT ? ';
1037         push @query_params, $params->{'limit'};
1038     }
1039
1040     my $sth = $dbh->prepare( $statement );
1041     my $result = $sth->execute( @query_params );
1042     return $sth->fetchall_arrayref({});
1043 }
1044
1045 =head2 GetMessageTransportTypes
1046
1047   my @mtt = GetMessageTransportTypes();
1048
1049   returns an arrayref of transport types
1050
1051 =cut
1052
1053 sub GetMessageTransportTypes {
1054     my $dbh = C4::Context->dbh();
1055     my $mtts = $dbh->selectcol_arrayref("
1056         SELECT message_transport_type
1057         FROM message_transport_types
1058         ORDER BY message_transport_type
1059     ");
1060     return $mtts;
1061 }
1062
1063 =head2 _add_attachements
1064
1065 named parameters:
1066 letter - the standard letter hashref
1067 attachments - listref of attachments. each attachment is a hashref of:
1068   type - the mime type, like 'text/plain'
1069   content - the actual attachment
1070   filename - the name of the attachment.
1071 message - a MIME::Lite object to attach these to.
1072
1073 returns your letter object, with the content updated.
1074
1075 =cut
1076
1077 sub _add_attachments {
1078     my $params = shift;
1079
1080     my $letter = $params->{'letter'};
1081     my $attachments = $params->{'attachments'};
1082     return $letter unless @$attachments;
1083     my $message = $params->{'message'};
1084
1085     # First, we have to put the body in as the first attachment
1086     $message->attach(
1087         Type => $letter->{'content-type'} || 'TEXT',
1088         Data => $letter->{'is_html'}
1089             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1090             : $letter->{'content'},
1091     );
1092
1093     foreach my $attachment ( @$attachments ) {
1094         $message->attach(
1095             Type     => $attachment->{'type'},
1096             Data     => $attachment->{'content'},
1097             Filename => $attachment->{'filename'},
1098         );
1099     }
1100     # we're forcing list context here to get the header, not the count back from grep.
1101     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1102     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1103     $letter->{'content'} = $message->body_as_string;
1104
1105     return $letter;
1106
1107 }
1108
1109 sub _get_unsent_messages {
1110     my $params = shift;
1111
1112     my $dbh = C4::Context->dbh();
1113     my $statement = << 'ENDSQL';
1114 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
1115   FROM message_queue mq
1116   LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1117  WHERE status = ?
1118 ENDSQL
1119
1120     my @query_params = ('pending');
1121     if ( ref $params ) {
1122         if ( $params->{'message_transport_type'} ) {
1123             $statement .= ' AND message_transport_type = ? ';
1124             push @query_params, $params->{'message_transport_type'};
1125         }
1126         if ( $params->{'borrowernumber'} ) {
1127             $statement .= ' AND borrowernumber = ? ';
1128             push @query_params, $params->{'borrowernumber'};
1129         }
1130         if ( $params->{'limit'} ) {
1131             $statement .= ' limit ? ';
1132             push @query_params, $params->{'limit'};
1133         }
1134     }
1135
1136     $debug and warn "_get_unsent_messages SQL: $statement";
1137     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1138     my $sth = $dbh->prepare( $statement );
1139     my $result = $sth->execute( @query_params );
1140     return $sth->fetchall_arrayref({});
1141 }
1142
1143 sub _send_message_by_email {
1144     my $message = shift or return;
1145     my ($username, $password, $method) = @_;
1146
1147     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1148     my $to_address = $message->{'to_address'};
1149     unless ($to_address) {
1150         unless ($member) {
1151             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1152             _set_message_status( { message_id => $message->{'message_id'},
1153                                    status     => 'failed' } );
1154             return;
1155         }
1156         $to_address = C4::Members::GetNoticeEmailAddress( $message->{'borrowernumber'} );
1157         unless ($to_address) {  
1158             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1159             # warning too verbose for this more common case?
1160             _set_message_status( { message_id => $message->{'message_id'},
1161                                    status     => 'failed' } );
1162             return;
1163         }
1164     }
1165
1166     my $utf8   = decode('MIME-Header', $message->{'subject'} );
1167     $message->{subject}= encode('MIME-Header', $utf8);
1168     my $subject = encode('UTF-8', $message->{'subject'});
1169     my $content = encode('UTF-8', $message->{'content'});
1170     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1171     my $is_html = $content_type =~ m/html/io;
1172     my $branch_email = undef;
1173     my $branch_replyto = undef;
1174     my $branch_returnpath = undef;
1175     if ($member){
1176         my $branchdetail = GetBranchDetail( $member->{'branchcode'} );
1177         $branch_email = $branchdetail->{'branchemail'};
1178         $branch_replyto = $branchdetail->{'branchreplyto'};
1179         $branch_returnpath = $branchdetail->{'branchreturnpath'};
1180     }
1181     my $email = Koha::Email->new();
1182     my %sendmail_params = $email->create_message_headers(
1183         {
1184             to      => $to_address,
1185             from    => $message->{'from_address'} || $branch_email,
1186             replyto => $branch_replyto,
1187             sender  => $branch_returnpath,
1188             subject => $subject,
1189             message => $is_html ? _wrap_html( $content, $subject ) : $content,
1190             contenttype => $content_type
1191         }
1192     );
1193
1194     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1195     if ( my $bcc = C4::Context->preference('OverdueNoticeBcc') ) {
1196        $sendmail_params{ Bcc } = $bcc;
1197     }
1198
1199     _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
1200     if ( sendmail( %sendmail_params ) ) {
1201         _set_message_status( { message_id => $message->{'message_id'},
1202                 status     => 'sent' } );
1203         return 1;
1204     } else {
1205         _set_message_status( { message_id => $message->{'message_id'},
1206                 status     => 'failed' } );
1207         carp $Mail::Sendmail::error;
1208         return;
1209     }
1210 }
1211
1212 sub _wrap_html {
1213     my ($content, $title) = @_;
1214
1215     my $css = C4::Context->preference("NoticeCSS") || '';
1216     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1217     return <<EOS;
1218 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1219     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1220 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1221 <head>
1222 <title>$title</title>
1223 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1224 $css
1225 </head>
1226 <body>
1227 $content
1228 </body>
1229 </html>
1230 EOS
1231 }
1232
1233 sub _is_duplicate {
1234     my ( $message ) = @_;
1235     my $dbh = C4::Context->dbh;
1236     my $count = $dbh->selectrow_array(q|
1237         SELECT COUNT(*)
1238         FROM message_queue
1239         WHERE message_transport_type = ?
1240         AND borrowernumber = ?
1241         AND letter_code = ?
1242         AND CAST(time_queued AS date) = CAST(NOW() AS date)
1243         AND status="sent"
1244         AND content = ?
1245     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1246     return $count;
1247 }
1248
1249 sub _send_message_by_sms {
1250     my $message = shift or return;
1251     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1252
1253     unless ( $member->{smsalertnumber} ) {
1254         _set_message_status( { message_id => $message->{'message_id'},
1255                                status     => 'failed' } );
1256         return;
1257     }
1258
1259     if ( _is_duplicate( $message ) ) {
1260         _set_message_status( { message_id => $message->{'message_id'},
1261                                status     => 'failed' } );
1262         return;
1263     }
1264
1265     my $success = C4::SMS->send_sms( { destination => $member->{'smsalertnumber'},
1266                                        message     => $message->{'content'},
1267                                      } );
1268     _set_message_status( { message_id => $message->{'message_id'},
1269                            status     => ($success ? 'sent' : 'failed') } );
1270     return $success;
1271 }
1272
1273 sub _update_message_to_address {
1274     my ($id, $to)= @_;
1275     my $dbh = C4::Context->dbh();
1276     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1277 }
1278
1279 sub _set_message_status {
1280     my $params = shift or return;
1281
1282     foreach my $required_parameter ( qw( message_id status ) ) {
1283         return unless exists $params->{ $required_parameter };
1284     }
1285
1286     my $dbh = C4::Context->dbh();
1287     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1288     my $sth = $dbh->prepare( $statement );
1289     my $result = $sth->execute( $params->{'status'},
1290                                 $params->{'message_id'} );
1291     return $result;
1292 }
1293
1294
1295 1;
1296 __END__