Bug 14107: Patron cards: Make barcode width and height scaling editable
[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 biblionumber
394         my $sth =
395           $dbh->prepare(
396             "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
397         $sth->execute($externalid);
398         my ($biblionumber) = $sth->fetchrow
399           or warn( "No subscription for '$externalid'" ),
400              return;
401
402         my %letter;
403         # find the list of borrowers to alert
404         my $alerts = getalert( '', 'issue', $externalid );
405         foreach (@$alerts) {
406             my $borinfo = C4::Members::GetMember('borrowernumber' => $_->{'borrowernumber'});
407             my $email = $borinfo->{email} or next;
408
409 #                    warn "sending issues...";
410             my $userenv = C4::Context->userenv;
411             my $branchdetails = GetBranchDetail($_->{'branchcode'});
412             my $letter = GetPreparedLetter (
413                 module => 'serial',
414                 letter_code => $letter_code,
415                 branchcode => $userenv->{branch},
416                 tables => {
417                     'branches'    => $_->{branchcode},
418                     'biblio'      => $biblionumber,
419                     'biblioitems' => $biblionumber,
420                     'borrowers'   => $borinfo,
421                 },
422                 want_librarian => 1,
423             ) or return;
424
425             # ... then send mail
426             my $message = Koha::Email->new();
427             my %mail = $message->create_message_headers(
428                 {
429                     to      => $email,
430                     from    => $branchdetails->{'branchemail'},
431                     replyto => $branchdetails->{'branchreplyto'},
432                     sender  => $branchdetails->{'branchreturnpath'},
433                     subject => Encode::encode( "UTF-8", "" . $letter->{title} ),
434                     message => $letter->{'is_html'}
435                                 ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
436                                               Encode::encode( "UTF-8", "" . $letter->{'title'} ))
437                                 : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
438                     contenttype => $letter->{'is_html'}
439                                     ? 'text/html; charset="utf-8"'
440                                     : 'text/plain; charset="utf-8"',
441                 }
442             );
443             sendmail(%mail) or carp $Mail::Sendmail::error;
444         }
445     }
446     elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' ) {
447
448         # prepare the letter...
449         # search the biblionumber
450         my $strsth =  $type eq 'claimacquisition'
451             ? qq{
452             SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
453             FROM aqorders
454             LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
455             LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
456             LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
457             WHERE aqorders.ordernumber IN (
458             }
459             : qq{
460             SELECT serial.*,subscription.*, biblio.*, aqbooksellers.*,
461             aqbooksellers.id AS booksellerid
462             FROM serial
463             LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
464             LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
465             LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
466             WHERE serial.serialid IN (
467             };
468
469         if (!@$externalid){
470             carp "No Order seleted";
471             return { error => "no_order_seleted" };
472         }
473
474         $strsth .= join( ",", @$externalid ) . ")";
475         my $sthorders = $dbh->prepare($strsth);
476         $sthorders->execute;
477         my $dataorders = $sthorders->fetchall_arrayref( {} );
478
479         my $sthbookseller =
480           $dbh->prepare("select * from aqbooksellers where id=?");
481         $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
482         my $databookseller = $sthbookseller->fetchrow_hashref;
483         my $addressee =  $type eq 'claimacquisition' ? 'acqprimary' : 'serialsprimary';
484         my $sthcontact =
485           $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
486         $sthcontact->execute( $dataorders->[0]->{booksellerid} );
487         my $datacontact = $sthcontact->fetchrow_hashref;
488
489         my @email;
490         my @cc;
491         push @email, $databookseller->{bookselleremail} if $databookseller->{bookselleremail};
492         push @email, $datacontact->{email}           if ( $datacontact && $datacontact->{email} );
493         unless (@email) {
494             warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
495             return { error => "no_email" };
496         }
497         my $addlcontact;
498         while ($addlcontact = $sthcontact->fetchrow_hashref) {
499             push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
500         }
501
502         my $userenv = C4::Context->userenv;
503         my $letter = GetPreparedLetter (
504             module => $type,
505             letter_code => $letter_code,
506             branchcode => $userenv->{branch},
507             tables => {
508                 'branches'    => $userenv->{branch},
509                 'aqbooksellers' => $databookseller,
510                 'aqcontacts'    => $datacontact,
511             },
512             repeat => $dataorders,
513             want_librarian => 1,
514         ) or return;
515
516         # Remove the order tag
517         $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
518
519         # ... then send mail
520         my %mail = (
521             To => join( ',', @email),
522             Cc             => join( ',', @cc),
523             From           => $userenv->{emailaddress},
524             Subject        => Encode::encode( "UTF-8", "" . $letter->{title} ),
525             Message => $letter->{'is_html'}
526                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
527                                           Encode::encode( "UTF-8", "" . $letter->{'title'} ))
528                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
529             'Content-Type' => $letter->{'is_html'}
530                                 ? 'text/html; charset="utf-8"'
531                                 : 'text/plain; charset="utf-8"',
532         );
533
534         $mail{'Reply-to'} = C4::Context->preference('ReplytoDefault')
535           if C4::Context->preference('ReplytoDefault');
536         $mail{'Sender'} = C4::Context->preference('ReturnpathDefault')
537           if C4::Context->preference('ReturnpathDefault');
538
539         unless ( sendmail(%mail) ) {
540             carp $Mail::Sendmail::error;
541             return { error => $Mail::Sendmail::error };
542         }
543
544         logaction(
545             "ACQUISITION",
546             $type eq 'claimissues' ? "CLAIM ISSUE" : "ACQUISITION CLAIM",
547             undef,
548             "To="
549                 . join( ',', @email )
550                 . " Title="
551                 . $letter->{title}
552                 . " Content="
553                 . $letter->{content}
554         ) if C4::Context->preference("LetterLog");
555     }
556    # send an "account details" notice to a newly created user
557     elsif ( $type eq 'members' ) {
558         my $branchdetails = GetBranchDetail($externalid->{'branchcode'});
559         my $letter = GetPreparedLetter (
560             module => 'members',
561             letter_code => $letter_code,
562             branchcode => $externalid->{'branchcode'},
563             tables => {
564                 'branches'    => $branchdetails,
565                 'borrowers' => $externalid->{'borrowernumber'},
566             },
567             substitute => { 'borrowers.password' => $externalid->{'password'} },
568             want_librarian => 1,
569         ) or return;
570         return { error => "no_email" } unless $externalid->{'emailaddr'};
571         my $email = Koha::Email->new();
572         my %mail  = $email->create_message_headers(
573             {
574                 to      => $externalid->{'emailaddr'},
575                 from    => $branchdetails->{'branchemail'},
576                 replyto => $branchdetails->{'branchreplyto'},
577                 sender  => $branchdetails->{'branchreturnpath'},
578                 subject => Encode::encode( "UTF-8", "" . $letter->{'title'} ),
579                 message => $letter->{'is_html'}
580                             ? _wrap_html( Encode::encode( "UTF-8", $letter->{'content'} ),
581                                           Encode::encode( "UTF-8", "" . $letter->{'title'}  ) )
582                             : Encode::encode( "UTF-8", "" . $letter->{'content'} ),
583                 contenttype => $letter->{'is_html'}
584                                 ? 'text/html; charset="utf-8"'
585                                 : 'text/plain; charset="utf-8"',
586             }
587         );
588         sendmail(%mail) or carp $Mail::Sendmail::error;
589     }
590 }
591
592 =head2 GetPreparedLetter( %params )
593
594     %params hash:
595       module => letter module, mandatory
596       letter_code => letter code, mandatory
597       branchcode => for letter selection, if missing default system letter taken
598       tables => a hashref with table names as keys. Values are either:
599         - a scalar - primary key value
600         - an arrayref - primary key values
601         - a hashref - full record
602       substitute => custom substitution key/value pairs
603       repeat => records to be substituted on consecutive lines:
604         - an arrayref - tries to guess what needs substituting by
605           taking remaining << >> tokensr; not recommended
606         - a hashref token => @tables - replaces <token> << >> << >> </token>
607           subtemplate for each @tables row; table is a hashref as above
608       want_librarian => boolean,  if set to true triggers librarian details
609         substitution from the userenv
610     Return value:
611       letter fields hashref (title & content useful)
612
613 =cut
614
615 sub GetPreparedLetter {
616     my %params = @_;
617
618     my $module      = $params{module} or croak "No module";
619     my $letter_code = $params{letter_code} or croak "No letter_code";
620     my $branchcode  = $params{branchcode} || '';
621     my $mtt         = $params{message_transport_type} || 'email';
622
623     my $letter = getletter( $module, $letter_code, $branchcode, $mtt )
624         or warn( "No $module $letter_code letter transported by " . $mtt ),
625             return;
626
627     my $tables = $params{tables};
628     my $substitute = $params{substitute};
629     my $repeat = $params{repeat};
630     $tables || $substitute || $repeat
631       or carp( "ERROR: nothing to substitute - both 'tables' and 'substitute' are empty" ),
632          return;
633     my $want_librarian = $params{want_librarian};
634
635     if ($substitute) {
636         while ( my ($token, $val) = each %$substitute ) {
637             if ( $token eq 'items.content' ) {
638                 $val =~ s|\n|<br/>|g if $letter->{is_html};
639             }
640
641             $letter->{title} =~ s/<<$token>>/$val/g;
642             $letter->{content} =~ s/<<$token>>/$val/g;
643        }
644     }
645
646     my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
647     $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
648
649     if ($want_librarian) {
650         # parsing librarian name
651         my $userenv = C4::Context->userenv;
652         $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
653         $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
654         $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
655     }
656
657     my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
658
659     if ($repeat) {
660         if (ref ($repeat) eq 'ARRAY' ) {
661             $repeat_no_enclosing_tags = $repeat;
662         } else {
663             $repeat_enclosing_tags = $repeat;
664         }
665     }
666
667     if ($repeat_enclosing_tags) {
668         while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
669             if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
670                 my $subcontent = $1;
671                 my @lines = map {
672                     my %subletter = ( title => '', content => $subcontent );
673                     _substitute_tables( \%subletter, $_ );
674                     $subletter{content};
675                 } @$tag_tables;
676                 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
677             }
678         }
679     }
680
681     if ($tables) {
682         _substitute_tables( $letter, $tables );
683     }
684
685     if ($repeat_no_enclosing_tags) {
686         if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
687             my $line = $&;
688             my $i = 1;
689             my @lines = map {
690                 my $c = $line;
691                 $c =~ s/<<count>>/$i/go;
692                 foreach my $field ( keys %{$_} ) {
693                     $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
694                 }
695                 $i++;
696                 $c;
697             } @$repeat_no_enclosing_tags;
698
699             my $replaceby = join( "\n", @lines );
700             $letter->{content} =~ s/\Q$line\E/$replaceby/s;
701         }
702     }
703
704     $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
705 #   $letter->{content} =~ s/<<[^>]*>>//go;
706
707     return $letter;
708 }
709
710 sub _substitute_tables {
711     my ( $letter, $tables ) = @_;
712     while ( my ($table, $param) = each %$tables ) {
713         next unless $param;
714
715         my $ref = ref $param;
716
717         my $values;
718         if ($ref && $ref eq 'HASH') {
719             $values = $param;
720         }
721         else {
722             my $sth = _parseletter_sth($table);
723             unless ($sth) {
724                 warn "_parseletter_sth('$table') failed to return a valid sth.  No substitution will be done for that table.";
725                 return;
726             }
727             $sth->execute( $ref ? @$param : $param );
728
729             $values = $sth->fetchrow_hashref;
730             $sth->finish();
731         }
732
733         _parseletter ( $letter, $table, $values );
734     }
735 }
736
737 sub _parseletter_sth {
738     my $table = shift;
739     my $sth;
740     unless ($table) {
741         carp "ERROR: _parseletter_sth() called without argument (table)";
742         return;
743     }
744     # NOTE: we used to check whether we had a statement handle cached in
745     #       a %handles module-level variable. This was a dumb move and
746     #       broke things for the rest of us. prepare_cached is a better
747     #       way to cache statement handles anyway.
748     my $query = 
749     ($table eq 'biblio'       ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
750     ($table eq 'biblioitems'  ) ? "SELECT * FROM $table WHERE   biblionumber = ?"                                  :
751     ($table eq 'items'        ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
752     ($table eq 'issues'       ) ? "SELECT * FROM $table WHERE     itemnumber = ?"                                  :
753     ($table eq 'old_issues'   ) ? "SELECT * FROM $table WHERE     itemnumber = ? ORDER BY timestamp DESC LIMIT 1"  :
754     ($table eq 'reserves'     ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?"             :
755     ($table eq 'borrowers'    ) ? "SELECT * FROM $table WHERE borrowernumber = ?"                                  :
756     ($table eq 'branches'     ) ? "SELECT * FROM $table WHERE     branchcode = ?"                                  :
757     ($table eq 'suggestions'  ) ? "SELECT * FROM $table WHERE   suggestionid = ?"                                  :
758     ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE             id = ?"                                  :
759     ($table eq 'aqorders'     ) ? "SELECT * FROM $table WHERE    ordernumber = ?"                                  :
760     ($table eq 'opac_news'    ) ? "SELECT * FROM $table WHERE          idnew = ?"                                  :
761     ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
762     undef ;
763     unless ($query) {
764         warn "ERROR: No _parseletter_sth query for table '$table'";
765         return;     # nothing to get
766     }
767     unless ($sth = C4::Context->dbh->prepare_cached($query)) {
768         warn "ERROR: Failed to prepare query: '$query'";
769         return;
770     }
771     return $sth;    # now cache is populated for that $table
772 }
773
774 =head2 _parseletter($letter, $table, $values)
775
776     parameters :
777     - $letter : a hash to letter fields (title & content useful)
778     - $table : the Koha table to parse.
779     - $values : table record hashref
780     parse all fields from a table, and replace values in title & content with the appropriate value
781     (not exported sub, used only internally)
782
783 =cut
784
785 sub _parseletter {
786     my ( $letter, $table, $values ) = @_;
787
788     if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
789         my @waitingdate = split /-/, $values->{'waitingdate'};
790
791         $values->{'expirationdate'} = '';
792         if( C4::Context->preference('ExpireReservesMaxPickUpDelay') &&
793         C4::Context->preference('ReservesMaxPickUpDelay') ) {
794             my $dt = dt_from_string();
795             $dt->add( days => C4::Context->preference('ReservesMaxPickUpDelay') );
796             $values->{'expirationdate'} = output_pref({ dt => $dt, dateonly => 1 });
797         }
798
799         $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
800
801     }
802
803     if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
804         my $todaysdate = output_pref( DateTime->now() );
805         $letter->{content} =~ s/<<today>>/$todaysdate/go;
806     }
807
808     while ( my ($field, $val) = each %$values ) {
809         my $replacetablefield = "<<$table.$field>>";
810         my $replacefield = "<<$field>>";
811         $val =~ s/\p{P}$// if $val && $table=~/biblio/;
812             #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
813             #Therefore adding the test on biblio. This includes biblioitems,
814             #but excludes items. Removed unneeded global and lookahead.
815
816         $val = GetAuthorisedValueByCode ('ROADTYPE', $val, 0) if $table=~/^borrowers$/ && $field=~/^streettype$/;
817         my $replacedby   = defined ($val) ? $val : '';
818         if (    $replacedby
819             and not $replacedby =~ m|0000-00-00|
820             and not $replacedby =~ m|9999-12-31|
821             and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
822         {
823             # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
824             my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
825             eval {
826                 $replacedby = output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
827             };
828             warn "$replacedby seems to be a date but an error occurs on generating it ($@)" if $@;
829         }
830         ($letter->{title}  ) and do {
831             $letter->{title}   =~ s/$replacetablefield/$replacedby/g;
832             $letter->{title}   =~ s/$replacefield/$replacedby/g;
833         };
834         ($letter->{content}) and do {
835             $letter->{content} =~ s/$replacetablefield/$replacedby/g;
836             $letter->{content} =~ s/$replacefield/$replacedby/g;
837         };
838     }
839
840     if ($table eq 'borrowers' && $letter->{content}) {
841         if ( my $attributes = GetBorrowerAttributes($values->{borrowernumber}) ) {
842             my %attr;
843             foreach (@$attributes) {
844                 my $code = $_->{code};
845                 my $val  = $_->{value_description} || $_->{value};
846                 $val =~ s/\p{P}(?=$)//g if $val;
847                 next unless $val gt '';
848                 $attr{$code} ||= [];
849                 push @{ $attr{$code} }, $val;
850             }
851             while ( my ($code, $val_ar) = each %attr ) {
852                 my $replacefield = "<<borrower-attribute:$code>>";
853                 my $replacedby   = join ',', @$val_ar;
854                 $letter->{content} =~ s/$replacefield/$replacedby/g;
855             }
856         }
857     }
858     return $letter;
859 }
860
861 =head2 EnqueueLetter
862
863   my $success = EnqueueLetter( { letter => $letter, 
864         borrowernumber => '12', message_transport_type => 'email' } )
865
866 places a letter in the message_queue database table, which will
867 eventually get processed (sent) by the process_message_queue.pl
868 cronjob when it calls SendQueuedMessages.
869
870 return message_id on success
871
872 =cut
873
874 sub EnqueueLetter {
875     my $params = shift or return;
876
877     return unless exists $params->{'letter'};
878 #   return unless exists $params->{'borrowernumber'};
879     return unless exists $params->{'message_transport_type'};
880
881     my $content = $params->{letter}->{content};
882     $content =~ s/\s+//g if(defined $content);
883     if ( not defined $content or $content eq '' ) {
884         warn "Trying to add an empty message to the message queue" if $debug;
885         return;
886     }
887
888     # If we have any attachments we should encode then into the body.
889     if ( $params->{'attachments'} ) {
890         $params->{'letter'} = _add_attachments(
891             {   letter      => $params->{'letter'},
892                 attachments => $params->{'attachments'},
893                 message     => MIME::Lite->new( Type => 'multipart/mixed' ),
894             }
895         );
896     }
897
898     my $dbh       = C4::Context->dbh();
899     my $statement = << 'ENDSQL';
900 INSERT INTO message_queue
901 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, content_type )
902 VALUES
903 ( ?,              ?,       ?,       ?,        ?,           ?,                      ?,      NOW(),       ?,          ?,            ? )
904 ENDSQL
905
906     my $sth    = $dbh->prepare($statement);
907     my $result = $sth->execute(
908         $params->{'borrowernumber'},              # borrowernumber
909         $params->{'letter'}->{'title'},           # subject
910         $params->{'letter'}->{'content'},         # content
911         $params->{'letter'}->{'metadata'} || '',  # metadata
912         $params->{'letter'}->{'code'}     || '',  # letter_code
913         $params->{'message_transport_type'},      # message_transport_type
914         'pending',                                # status
915         $params->{'to_address'},                  # to_address
916         $params->{'from_address'},                # from_address
917         $params->{'letter'}->{'content-type'},    # content_type
918     );
919     return $dbh->last_insert_id(undef,undef,'message_queue', undef);
920 }
921
922 =head2 SendQueuedMessages ([$hashref]) 
923
924   my $sent = SendQueuedMessages( { verbose => 1 } );
925
926 sends all of the 'pending' items in the message queue.
927
928 returns number of messages sent.
929
930 =cut
931
932 sub SendQueuedMessages {
933     my $params = shift;
934
935     my $unsent_messages = _get_unsent_messages();
936     MESSAGE: foreach my $message ( @$unsent_messages ) {
937         # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
938         warn sprintf( 'sending %s message to patron: %s',
939                       $message->{'message_transport_type'},
940                       $message->{'borrowernumber'} || 'Admin' )
941           if $params->{'verbose'} or $debug;
942         # This is just begging for subclassing
943         next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
944         if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
945             _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
946         }
947         elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
948             _send_message_by_sms( $message );
949         }
950     }
951     return scalar( @$unsent_messages );
952 }
953
954 =head2 GetRSSMessages
955
956   my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
957
958 returns a listref of all queued RSS messages for a particular person.
959
960 =cut
961
962 sub GetRSSMessages {
963     my $params = shift;
964
965     return unless $params;
966     return unless ref $params;
967     return unless $params->{'borrowernumber'};
968     
969     return _get_unsent_messages( { message_transport_type => 'rss',
970                                    limit                  => $params->{'limit'},
971                                    borrowernumber         => $params->{'borrowernumber'}, } );
972 }
973
974 =head2 GetPrintMessages
975
976   my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
977
978 Returns a arrayref of all queued print messages (optionally, for a particular
979 person).
980
981 =cut
982
983 sub GetPrintMessages {
984     my $params = shift || {};
985     
986     return _get_unsent_messages( { message_transport_type => 'print',
987                                    borrowernumber         => $params->{'borrowernumber'},
988                                  } );
989 }
990
991 =head2 GetQueuedMessages ([$hashref])
992
993   my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
994
995 fetches messages out of the message queue.
996
997 returns:
998 list of hashes, each has represents a message in the message queue.
999
1000 =cut
1001
1002 sub GetQueuedMessages {
1003     my $params = shift;
1004
1005     my $dbh = C4::Context->dbh();
1006     my $statement = << 'ENDSQL';
1007 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued
1008 FROM message_queue
1009 ENDSQL
1010
1011     my @query_params;
1012     my @whereclauses;
1013     if ( exists $params->{'borrowernumber'} ) {
1014         push @whereclauses, ' borrowernumber = ? ';
1015         push @query_params, $params->{'borrowernumber'};
1016     }
1017
1018     if ( @whereclauses ) {
1019         $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1020     }
1021
1022     if ( defined $params->{'limit'} ) {
1023         $statement .= ' LIMIT ? ';
1024         push @query_params, $params->{'limit'};
1025     }
1026
1027     my $sth = $dbh->prepare( $statement );
1028     my $result = $sth->execute( @query_params );
1029     return $sth->fetchall_arrayref({});
1030 }
1031
1032 =head2 GetMessageTransportTypes
1033
1034   my @mtt = GetMessageTransportTypes();
1035
1036   returns an arrayref of transport types
1037
1038 =cut
1039
1040 sub GetMessageTransportTypes {
1041     my $dbh = C4::Context->dbh();
1042     my $mtts = $dbh->selectcol_arrayref("
1043         SELECT message_transport_type
1044         FROM message_transport_types
1045         ORDER BY message_transport_type
1046     ");
1047     return $mtts;
1048 }
1049
1050 =head2 _add_attachements
1051
1052 named parameters:
1053 letter - the standard letter hashref
1054 attachments - listref of attachments. each attachment is a hashref of:
1055   type - the mime type, like 'text/plain'
1056   content - the actual attachment
1057   filename - the name of the attachment.
1058 message - a MIME::Lite object to attach these to.
1059
1060 returns your letter object, with the content updated.
1061
1062 =cut
1063
1064 sub _add_attachments {
1065     my $params = shift;
1066
1067     my $letter = $params->{'letter'};
1068     my $attachments = $params->{'attachments'};
1069     return $letter unless @$attachments;
1070     my $message = $params->{'message'};
1071
1072     # First, we have to put the body in as the first attachment
1073     $message->attach(
1074         Type => $letter->{'content-type'} || 'TEXT',
1075         Data => $letter->{'is_html'}
1076             ? _wrap_html($letter->{'content'}, $letter->{'title'})
1077             : $letter->{'content'},
1078     );
1079
1080     foreach my $attachment ( @$attachments ) {
1081         $message->attach(
1082             Type     => $attachment->{'type'},
1083             Data     => $attachment->{'content'},
1084             Filename => $attachment->{'filename'},
1085         );
1086     }
1087     # we're forcing list context here to get the header, not the count back from grep.
1088     ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1089     $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1090     $letter->{'content'} = $message->body_as_string;
1091
1092     return $letter;
1093
1094 }
1095
1096 sub _get_unsent_messages {
1097     my $params = shift;
1098
1099     my $dbh = C4::Context->dbh();
1100     my $statement = << 'ENDSQL';
1101 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
1102   FROM message_queue mq
1103   LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1104  WHERE status = ?
1105 ENDSQL
1106
1107     my @query_params = ('pending');
1108     if ( ref $params ) {
1109         if ( $params->{'message_transport_type'} ) {
1110             $statement .= ' AND message_transport_type = ? ';
1111             push @query_params, $params->{'message_transport_type'};
1112         }
1113         if ( $params->{'borrowernumber'} ) {
1114             $statement .= ' AND borrowernumber = ? ';
1115             push @query_params, $params->{'borrowernumber'};
1116         }
1117         if ( $params->{'limit'} ) {
1118             $statement .= ' limit ? ';
1119             push @query_params, $params->{'limit'};
1120         }
1121     }
1122
1123     $debug and warn "_get_unsent_messages SQL: $statement";
1124     $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1125     my $sth = $dbh->prepare( $statement );
1126     my $result = $sth->execute( @query_params );
1127     return $sth->fetchall_arrayref({});
1128 }
1129
1130 sub _send_message_by_email {
1131     my $message = shift or return;
1132     my ($username, $password, $method) = @_;
1133
1134     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1135     my $to_address = $message->{'to_address'};
1136     unless ($to_address) {
1137         unless ($member) {
1138             warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1139             _set_message_status( { message_id => $message->{'message_id'},
1140                                    status     => 'failed' } );
1141             return;
1142         }
1143         $to_address = C4::Members::GetNoticeEmailAddress( $message->{'borrowernumber'} );
1144         unless ($to_address) {  
1145             # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1146             # warning too verbose for this more common case?
1147             _set_message_status( { message_id => $message->{'message_id'},
1148                                    status     => 'failed' } );
1149             return;
1150         }
1151     }
1152
1153     my $utf8   = decode('MIME-Header', $message->{'subject'} );
1154     $message->{subject}= encode('MIME-Header', $utf8);
1155     my $subject = encode('UTF-8', $message->{'subject'});
1156     my $content = encode('UTF-8', $message->{'content'});
1157     my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1158     my $is_html = $content_type =~ m/html/io;
1159     my $branch_email = undef;
1160     my $branch_replyto = undef;
1161     my $branch_returnpath = undef;
1162     if ($member){
1163         my $branchdetail = GetBranchDetail( $member->{'branchcode'} );
1164         $branch_email = $branchdetail->{'branchemail'};
1165         $branch_replyto = $branchdetail->{'branchreplyto'};
1166         $branch_returnpath = $branchdetail->{'branchreturnpath'};
1167     }
1168     my $email = Koha::Email->new();
1169     my %sendmail_params = $email->create_message_headers(
1170         {
1171             to      => $to_address,
1172             from    => $message->{'from_address'} || $branch_email,
1173             replyto => $branch_replyto,
1174             sender  => $branch_returnpath,
1175             subject => $subject,
1176             message => $is_html ? _wrap_html( $content, $subject ) : $content,
1177             contenttype => $content_type
1178         }
1179     );
1180
1181     $sendmail_params{'Auth'} = {user => $username, pass => $password, method => $method} if $username;
1182     if ( my $bcc = C4::Context->preference('OverdueNoticeBcc') ) {
1183        $sendmail_params{ Bcc } = $bcc;
1184     }
1185
1186     _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
1187     if ( sendmail( %sendmail_params ) ) {
1188         _set_message_status( { message_id => $message->{'message_id'},
1189                 status     => 'sent' } );
1190         return 1;
1191     } else {
1192         _set_message_status( { message_id => $message->{'message_id'},
1193                 status     => 'failed' } );
1194         carp $Mail::Sendmail::error;
1195         return;
1196     }
1197 }
1198
1199 sub _wrap_html {
1200     my ($content, $title) = @_;
1201
1202     my $css = C4::Context->preference("NoticeCSS") || '';
1203     $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1204     return <<EOS;
1205 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1206     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1207 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1208 <head>
1209 <title>$title</title>
1210 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1211 $css
1212 </head>
1213 <body>
1214 $content
1215 </body>
1216 </html>
1217 EOS
1218 }
1219
1220 sub _is_duplicate {
1221     my ( $message ) = @_;
1222     my $dbh = C4::Context->dbh;
1223     my $count = $dbh->selectrow_array(q|
1224         SELECT COUNT(*)
1225         FROM message_queue
1226         WHERE message_transport_type = ?
1227         AND borrowernumber = ?
1228         AND letter_code = ?
1229         AND CAST(time_queued AS date) = CAST(NOW() AS date)
1230         AND status="sent"
1231         AND content = ?
1232     |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1233     return $count;
1234 }
1235
1236 sub _send_message_by_sms {
1237     my $message = shift or return;
1238     my $member = C4::Members::GetMember( 'borrowernumber' => $message->{'borrowernumber'} );
1239
1240     unless ( $member->{smsalertnumber} ) {
1241         _set_message_status( { message_id => $message->{'message_id'},
1242                                status     => 'failed' } );
1243         return;
1244     }
1245
1246     if ( _is_duplicate( $message ) ) {
1247         _set_message_status( { message_id => $message->{'message_id'},
1248                                status     => 'failed' } );
1249         return;
1250     }
1251
1252     my $success = C4::SMS->send_sms( { destination => $member->{'smsalertnumber'},
1253                                        message     => $message->{'content'},
1254                                      } );
1255     _set_message_status( { message_id => $message->{'message_id'},
1256                            status     => ($success ? 'sent' : 'failed') } );
1257     return $success;
1258 }
1259
1260 sub _update_message_to_address {
1261     my ($id, $to)= @_;
1262     my $dbh = C4::Context->dbh();
1263     $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1264 }
1265
1266 sub _set_message_status {
1267     my $params = shift or return;
1268
1269     foreach my $required_parameter ( qw( message_id status ) ) {
1270         return unless exists $params->{ $required_parameter };
1271     }
1272
1273     my $dbh = C4::Context->dbh();
1274     my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1275     my $sth = $dbh->prepare( $statement );
1276     my $result = $sth->execute( $params->{'status'},
1277                                 $params->{'message_id'} );
1278     return $result;
1279 }
1280
1281
1282 1;
1283 __END__