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