Bug 22343: Remove traces of libmail-sendmail-perl
[koha.git] / C4 / Letters.pm
blob2bae52d34656d084a1c13089ea709c393e90e41c
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 Date::Calc qw( Add_Delta_Days );
24 use Encode;
25 use Carp;
26 use Template;
27 use Module::Load::Conditional qw(can_load);
29 use Try::Tiny;
31 use C4::Members;
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::Notice::Messages;
40 use Koha::Notice::Templates;
41 use Koha::DateUtils qw( format_sqldatetime dt_from_string );
42 use Koha::Patrons;
43 use Koha::SMTP::Servers;
44 use Koha::Subscriptions;
46 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
48 BEGIN {
49 require Exporter;
50 @ISA = qw(Exporter);
51 @EXPORT = qw(
52 &GetLetters &GetLettersAvailableForALibrary &GetLetterTemplates &DelLetter &GetPreparedLetter &GetWrappedLetter &SendAlerts &GetPrintMessages &GetMessageTransportTypes
56 =head1 NAME
58 C4::Letters - Give functions for Letters management
60 =head1 SYNOPSIS
62 use C4::Letters;
64 =head1 DESCRIPTION
66 "Letters" is the tool used in Koha to manage informations sent to the patrons and/or the library. This include some cron jobs like
67 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)
69 Letters are managed through "alerts" sent by Koha on some events. All "alert" related functions are in this module too.
71 =head2 GetLetters([$module])
73 $letters = &GetLetters($module);
74 returns informations about letters.
75 if needed, $module filters for letters given module
77 DEPRECATED - You must use Koha::Notice::Templates instead
78 The group by clause is confusing and can lead to issues
80 =cut
82 sub GetLetters {
83 my ($filters) = @_;
84 my $module = $filters->{module};
85 my $code = $filters->{code};
86 my $branchcode = $filters->{branchcode};
87 my $dbh = C4::Context->dbh;
88 my $letters = $dbh->selectall_arrayref(
90 SELECT code, module, name
91 FROM letter
92 WHERE 1
94 . ( $module ? q| AND module = ?| : q|| )
95 . ( $code ? q| AND code = ?| : q|| )
96 . ( defined $branchcode ? q| AND branchcode = ?| : q|| )
97 . q| GROUP BY code, module, name ORDER BY name|, { Slice => {} }
98 , ( $module ? $module : () )
99 , ( $code ? $code : () )
100 , ( defined $branchcode ? $branchcode : () )
103 return $letters;
106 =head2 GetLetterTemplates
108 my $letter_templates = GetLetterTemplates(
110 module => 'circulation',
111 code => 'my code',
112 branchcode => 'CPL', # '' for default,
116 Return a hashref of letter templates.
118 =cut
120 sub GetLetterTemplates {
121 my ( $params ) = @_;
123 my $module = $params->{module};
124 my $code = $params->{code};
125 my $branchcode = $params->{branchcode} // '';
126 my $dbh = C4::Context->dbh;
127 return Koha::Notice::Templates->search(
129 module => $module,
130 code => $code,
131 branchcode => $branchcode,
133 C4::Context->preference('TranslateNotices')
134 ? ()
135 : ( lang => 'default' )
138 )->unblessed;
141 =head2 GetLettersAvailableForALibrary
143 my $letters = GetLettersAvailableForALibrary(
145 branchcode => 'CPL', # '' for default
146 module => 'circulation',
150 Return an arrayref of letters, sorted by name.
151 If a specific letter exist for the given branchcode, it will be retrieve.
152 Otherwise the default letter will be.
154 =cut
156 sub GetLettersAvailableForALibrary {
157 my ($filters) = @_;
158 my $branchcode = $filters->{branchcode};
159 my $module = $filters->{module};
161 croak "module should be provided" unless $module;
163 my $dbh = C4::Context->dbh;
164 my $default_letters = $dbh->selectall_arrayref(
166 SELECT module, code, branchcode, name
167 FROM letter
168 WHERE 1
170 . q| AND branchcode = ''|
171 . ( $module ? q| AND module = ?| : q|| )
172 . q| ORDER BY name|, { Slice => {} }
173 , ( $module ? $module : () )
176 my $specific_letters;
177 if ($branchcode) {
178 $specific_letters = $dbh->selectall_arrayref(
180 SELECT module, code, branchcode, name
181 FROM letter
182 WHERE 1
184 . q| AND branchcode = ?|
185 . ( $module ? q| AND module = ?| : q|| )
186 . q| ORDER BY name|, { Slice => {} }
187 , $branchcode
188 , ( $module ? $module : () )
192 my %letters;
193 for my $l (@$default_letters) {
194 $letters{ $l->{code} } = $l;
196 for my $l (@$specific_letters) {
197 # Overwrite the default letter with the specific one.
198 $letters{ $l->{code} } = $l;
201 return [ map { $letters{$_} }
202 sort { $letters{$a}->{name} cmp $letters{$b}->{name} }
203 keys %letters ];
207 sub getletter {
208 my ( $module, $code, $branchcode, $message_transport_type, $lang) = @_;
209 $message_transport_type //= '%';
210 $lang = 'default' unless( $lang && C4::Context->preference('TranslateNotices') );
213 my $only_my_library = C4::Context->only_my_library;
214 if ( $only_my_library and $branchcode ) {
215 $branchcode = C4::Context::mybranch();
217 $branchcode //= '';
219 my $dbh = C4::Context->dbh;
220 my $sth = $dbh->prepare(q{
221 SELECT *
222 FROM letter
223 WHERE module=? AND code=? AND (branchcode = ? OR branchcode = '')
224 AND message_transport_type LIKE ?
225 AND lang =?
226 ORDER BY branchcode DESC LIMIT 1
228 $sth->execute( $module, $code, $branchcode, $message_transport_type, $lang );
229 my $line = $sth->fetchrow_hashref
230 or return;
231 $line->{'content-type'} = 'text/html; charset="UTF-8"' if $line->{is_html};
232 return { %$line };
236 =head2 DelLetter
238 DelLetter(
240 branchcode => 'CPL',
241 module => 'circulation',
242 code => 'my code',
243 [ mtt => 'email', ]
247 Delete the letter. The mtt parameter is facultative.
248 If not given, all templates mathing the other parameters will be removed.
250 =cut
252 sub DelLetter {
253 my ($params) = @_;
254 my $branchcode = $params->{branchcode};
255 my $module = $params->{module};
256 my $code = $params->{code};
257 my $mtt = $params->{mtt};
258 my $lang = $params->{lang};
259 my $dbh = C4::Context->dbh;
260 $dbh->do(q|
261 DELETE FROM letter
262 WHERE branchcode = ?
263 AND module = ?
264 AND code = ?
266 . ( $mtt ? q| AND message_transport_type = ?| : q|| )
267 . ( $lang? q| AND lang = ?| : q|| )
268 , undef, $branchcode, $module, $code, ( $mtt ? $mtt : () ), ( $lang ? $lang : () ) );
271 =head2 SendAlerts
273 my $err = &SendAlerts($type, $externalid, $letter_code);
275 Parameters:
276 - $type : the type of alert
277 - $externalid : the id of the "object" to query
278 - $letter_code : the notice template to use
280 C<&SendAlerts> sends an email notice directly to a patron or a vendor.
282 Currently it supports ($type):
283 - claim serial issues (claimissues)
284 - claim acquisition orders (claimacquisition)
285 - send acquisition orders to the vendor (orderacquisition)
286 - notify patrons about newly received serial issues (issue)
287 - notify patrons when their account is created (members)
289 Returns undef or { error => 'message } on failure.
290 Returns true on success.
292 =cut
294 sub SendAlerts {
295 my ( $type, $externalid, $letter_code ) = @_;
296 my $dbh = C4::Context->dbh;
297 if ( $type eq 'issue' ) {
299 # prepare the letter...
300 # search the subscriptionid
301 my $sth =
302 $dbh->prepare(
303 "SELECT subscriptionid FROM serial WHERE serialid=?");
304 $sth->execute($externalid);
305 my ($subscriptionid) = $sth->fetchrow
306 or warn( "No subscription for '$externalid'" ),
307 return;
309 # search the biblionumber
310 $sth =
311 $dbh->prepare(
312 "SELECT biblionumber FROM subscription WHERE subscriptionid=?");
313 $sth->execute($subscriptionid);
314 my ($biblionumber) = $sth->fetchrow
315 or warn( "No biblionumber for '$subscriptionid'" ),
316 return;
318 # find the list of subscribers to notify
319 my $subscription = Koha::Subscriptions->find( $subscriptionid );
320 my $subscribers = $subscription->subscribers;
321 while ( my $patron = $subscribers->next ) {
322 my $email = $patron->email or next;
324 # warn "sending issues...";
325 my $userenv = C4::Context->userenv;
326 my $library = $patron->library;
327 my $letter = GetPreparedLetter (
328 module => 'serial',
329 letter_code => $letter_code,
330 branchcode => $userenv->{branch},
331 tables => {
332 'branches' => $library->branchcode,
333 'biblio' => $biblionumber,
334 'biblioitems' => $biblionumber,
335 'borrowers' => $patron->unblessed,
336 'subscription' => $subscriptionid,
337 'serial' => $externalid,
339 want_librarian => 1,
340 ) or return;
342 # FIXME: This 'default' behaviour should be moved to Koha::Email
343 my $mail = Koha::Email->create(
345 to => $email,
346 from => $library->branchemail,
347 reply_to => $library->branchreplyto,
348 sender => $library->branchreturnpath,
349 subject => "" . $letter->{title},
353 if ( $letter->{is_html} ) {
354 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
356 else {
357 $mail->text_body( $letter->{content} );
360 try {
361 $mail->send_or_die({ transport => $library->smtp_server->transport });
363 catch {
364 carp "$_";
365 return { error => "$_" };
369 elsif ( $type eq 'claimacquisition' or $type eq 'claimissues' or $type eq 'orderacquisition' ) {
371 # prepare the letter...
372 my $strsth;
373 my $sthorders;
374 my $dataorders;
375 my $action;
376 if ( $type eq 'claimacquisition') {
377 $strsth = qq{
378 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
379 FROM aqorders
380 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
381 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
382 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
383 WHERE aqorders.ordernumber IN (
386 if (!@$externalid){
387 carp "No order selected";
388 return { error => "no_order_selected" };
390 $strsth .= join( ",", ('?') x @$externalid ) . ")";
391 $action = "ACQUISITION CLAIM";
392 $sthorders = $dbh->prepare($strsth);
393 $sthorders->execute( @$externalid );
394 $dataorders = $sthorders->fetchall_arrayref( {} );
397 if ($type eq 'claimissues') {
398 $strsth = qq{
399 SELECT serial.*,subscription.*, biblio.*, biblioitems.*, aqbooksellers.*,
400 aqbooksellers.id AS booksellerid
401 FROM serial
402 LEFT JOIN subscription ON serial.subscriptionid=subscription.subscriptionid
403 LEFT JOIN biblio ON serial.biblionumber=biblio.biblionumber
404 LEFT JOIN biblioitems ON serial.biblionumber = biblioitems.biblionumber
405 LEFT JOIN aqbooksellers ON subscription.aqbooksellerid=aqbooksellers.id
406 WHERE serial.serialid IN (
409 if (!@$externalid){
410 carp "No issues selected";
411 return { error => "no_issues_selected" };
414 $strsth .= join( ",", ('?') x @$externalid ) . ")";
415 $action = "SERIAL CLAIM";
416 $sthorders = $dbh->prepare($strsth);
417 $sthorders->execute( @$externalid );
418 $dataorders = $sthorders->fetchall_arrayref( {} );
421 if ( $type eq 'orderacquisition') {
422 $strsth = qq{
423 SELECT aqorders.*,aqbasket.*,biblio.*,biblioitems.*
424 FROM aqorders
425 LEFT JOIN aqbasket ON aqbasket.basketno=aqorders.basketno
426 LEFT JOIN biblio ON aqorders.biblionumber=biblio.biblionumber
427 LEFT JOIN biblioitems ON aqorders.biblionumber=biblioitems.biblionumber
428 WHERE aqbasket.basketno = ?
429 AND orderstatus IN ('new','ordered')
432 if (!$externalid){
433 carp "No basketnumber given";
434 return { error => "no_basketno" };
436 $action = "ACQUISITION ORDER";
437 $sthorders = $dbh->prepare($strsth);
438 $sthorders->execute($externalid);
439 $dataorders = $sthorders->fetchall_arrayref( {} );
442 my $sthbookseller =
443 $dbh->prepare("select * from aqbooksellers where id=?");
444 $sthbookseller->execute( $dataorders->[0]->{booksellerid} );
445 my $databookseller = $sthbookseller->fetchrow_hashref;
447 my $addressee = $type eq 'claimacquisition' || $type eq 'orderacquisition' ? 'acqprimary' : 'serialsprimary';
449 my $sthcontact =
450 $dbh->prepare("SELECT * FROM aqcontacts WHERE booksellerid=? AND $type=1 ORDER BY $addressee DESC");
451 $sthcontact->execute( $dataorders->[0]->{booksellerid} );
452 my $datacontact = $sthcontact->fetchrow_hashref;
454 my @email;
455 my @cc;
456 push @email, $datacontact->{email} if ( $datacontact && $datacontact->{email} );
457 unless (@email) {
458 warn "Bookseller $dataorders->[0]->{booksellerid} without emails";
459 return { error => "no_email" };
461 my $addlcontact;
462 while ($addlcontact = $sthcontact->fetchrow_hashref) {
463 push @cc, $addlcontact->{email} if ( $addlcontact && $addlcontact->{email} );
466 my $userenv = C4::Context->userenv;
467 my $letter = GetPreparedLetter (
468 module => $type,
469 letter_code => $letter_code,
470 branchcode => $userenv->{branch},
471 tables => {
472 'branches' => $userenv->{branch},
473 'aqbooksellers' => $databookseller,
474 'aqcontacts' => $datacontact,
476 repeat => $dataorders,
477 want_librarian => 1,
478 ) or return { error => "no_letter" };
480 # Remove the order tag
481 $letter->{content} =~ s/<order>(.*?)<\/order>/$1/gxms;
483 # ... then send mail
484 my $library = Koha::Libraries->find( $userenv->{branch} );
485 my $mail = Koha::Email->create(
487 to => join( ',', @email ),
488 cc => join( ',', @cc ),
491 C4::Context->preference("ClaimsBccCopy")
492 && ( $type eq 'claimacquisition'
493 || $type eq 'claimissues' )
495 ? ( bcc => $userenv->{emailaddress} )
496 : ()
498 from => $library->branchemail
499 || C4::Context->preference('KohaAdminEmailAddress'),
500 subject => "" . $letter->{title},
504 if ( $letter->{is_html} ) {
505 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
507 else {
508 $mail->text_body( "" . $letter->{content} );
511 try {
512 $mail->send_or_die({ transport => $library->smtp_server->transport });
514 catch {
515 carp "$_";
516 return { error => "$_" };
519 logaction(
520 "ACQUISITION",
521 $action,
522 undef,
523 "To="
524 . join( ',', @email )
525 . " Title="
526 . $letter->{title}
527 . " Content="
528 . $letter->{content}
529 ) if C4::Context->preference("LetterLog");
531 # send an "account details" notice to a newly created user
532 elsif ( $type eq 'members' ) {
533 my $library = Koha::Libraries->find( $externalid->{branchcode} );
534 my $letter = GetPreparedLetter (
535 module => 'members',
536 letter_code => $letter_code,
537 branchcode => $externalid->{'branchcode'},
538 lang => $externalid->{lang} || 'default',
539 tables => {
540 'branches' => $library->unblessed,
541 'borrowers' => $externalid->{'borrowernumber'},
543 substitute => { 'borrowers.password' => $externalid->{'password'} },
544 want_librarian => 1,
545 ) or return;
546 return { error => "no_email" } unless $externalid->{'emailaddr'};
547 try {
549 # FIXME: This 'default' behaviour should be moved to Koha::Email
550 my $mail = Koha::Email->create(
552 to => $externalid->{'emailaddr'},
553 from => $library->branchemail,
554 reply_to => $library->branchreplyto,
555 sender => $library->branchreturnpath,
556 subject => "" . $letter->{'title'},
560 if ( $letter->{is_html} ) {
561 $mail->html_body( _wrap_html( $letter->{content}, "" . $letter->{title} ) );
563 else {
564 $mail->text_body( $letter->{content} );
567 $mail->send_or_die({ transport => $library->smtp_server->transport });
569 catch {
570 carp "$_";
571 return { error => "$_" };
575 # If we come here, return an OK status
576 return 1;
579 =head2 GetPreparedLetter( %params )
581 %params hash:
582 module => letter module, mandatory
583 letter_code => letter code, mandatory
584 branchcode => for letter selection, if missing default system letter taken
585 tables => a hashref with table names as keys. Values are either:
586 - a scalar - primary key value
587 - an arrayref - primary key values
588 - a hashref - full record
589 substitute => custom substitution key/value pairs
590 repeat => records to be substituted on consecutive lines:
591 - an arrayref - tries to guess what needs substituting by
592 taking remaining << >> tokensr; not recommended
593 - a hashref token => @tables - replaces <token> << >> << >> </token>
594 subtemplate for each @tables row; table is a hashref as above
595 want_librarian => boolean, if set to true triggers librarian details
596 substitution from the userenv
597 Return value:
598 letter fields hashref (title & content useful)
600 =cut
602 sub GetPreparedLetter {
603 my %params = @_;
605 my $letter = $params{letter};
607 unless ( $letter ) {
608 my $module = $params{module} or croak "No module";
609 my $letter_code = $params{letter_code} or croak "No letter_code";
610 my $branchcode = $params{branchcode} || '';
611 my $mtt = $params{message_transport_type} || 'email';
612 my $lang = $params{lang} || 'default';
614 $letter = getletter( $module, $letter_code, $branchcode, $mtt, $lang );
616 unless ( $letter ) {
617 $letter = getletter( $module, $letter_code, $branchcode, $mtt, 'default' )
618 or warn( "No $module $letter_code letter transported by " . $mtt ),
619 return;
623 my $tables = $params{tables} || {};
624 my $substitute = $params{substitute} || {};
625 my $loops = $params{loops} || {}; # loops is not supported for historical notices syntax
626 my $repeat = $params{repeat};
627 %$tables || %$substitute || $repeat || %$loops
628 or carp( "ERROR: nothing to substitute - both 'tables', 'loops' and 'substitute' are empty" ),
629 return;
630 my $want_librarian = $params{want_librarian};
632 if (%$substitute) {
633 while ( my ($token, $val) = each %$substitute ) {
634 if ( $token eq 'items.content' ) {
635 $val =~ s|\n|<br/>|g if $letter->{is_html};
638 $letter->{title} =~ s/<<$token>>/$val/g;
639 $letter->{content} =~ s/<<$token>>/$val/g;
643 my $OPACBaseURL = C4::Context->preference('OPACBaseURL');
644 $letter->{content} =~ s/<<OPACBaseURL>>/$OPACBaseURL/go;
646 if ($want_librarian) {
647 # parsing librarian name
648 my $userenv = C4::Context->userenv;
649 $letter->{content} =~ s/<<LibrarianFirstname>>/$userenv->{firstname}/go;
650 $letter->{content} =~ s/<<LibrarianSurname>>/$userenv->{surname}/go;
651 $letter->{content} =~ s/<<LibrarianEmailaddress>>/$userenv->{emailaddress}/go;
654 my ($repeat_no_enclosing_tags, $repeat_enclosing_tags);
656 if ($repeat) {
657 if (ref ($repeat) eq 'ARRAY' ) {
658 $repeat_no_enclosing_tags = $repeat;
659 } else {
660 $repeat_enclosing_tags = $repeat;
664 if ($repeat_enclosing_tags) {
665 while ( my ($tag, $tag_tables) = each %$repeat_enclosing_tags ) {
666 if ( $letter->{content} =~ m!<$tag>(.*)</$tag>!s ) {
667 my $subcontent = $1;
668 my @lines = map {
669 my %subletter = ( title => '', content => $subcontent );
670 _substitute_tables( \%subletter, $_ );
671 $subletter{content};
672 } @$tag_tables;
673 $letter->{content} =~ s!<$tag>.*</$tag>!join( "\n", @lines )!se;
678 if (%$tables) {
679 _substitute_tables( $letter, $tables );
682 if ($repeat_no_enclosing_tags) {
683 if ( $letter->{content} =~ m/[^\n]*<<.*>>[^\n]*/so ) {
684 my $line = $&;
685 my $i = 1;
686 my @lines = map {
687 my $c = $line;
688 $c =~ s/<<count>>/$i/go;
689 foreach my $field ( keys %{$_} ) {
690 $c =~ s/(<<[^\.]+.$field>>)/$_->{$field}/;
692 $i++;
694 } @$repeat_no_enclosing_tags;
696 my $replaceby = join( "\n", @lines );
697 $letter->{content} =~ s/\Q$line\E/$replaceby/s;
701 $letter->{content} = _process_tt(
703 content => $letter->{content},
704 tables => $tables,
705 loops => $loops,
706 substitute => $substitute,
710 $letter->{content} =~ s/<<\S*>>//go; #remove any stragglers
712 return $letter;
715 sub _substitute_tables {
716 my ( $letter, $tables ) = @_;
717 while ( my ($table, $param) = each %$tables ) {
718 next unless $param;
720 my $ref = ref $param;
722 my $values;
723 if ($ref && $ref eq 'HASH') {
724 $values = $param;
726 else {
727 my $sth = _parseletter_sth($table);
728 unless ($sth) {
729 warn "_parseletter_sth('$table') failed to return a valid sth. No substitution will be done for that table.";
730 return;
732 $sth->execute( $ref ? @$param : $param );
734 $values = $sth->fetchrow_hashref;
735 $sth->finish();
738 _parseletter ( $letter, $table, $values );
742 sub _parseletter_sth {
743 my $table = shift;
744 my $sth;
745 unless ($table) {
746 carp "ERROR: _parseletter_sth() called without argument (table)";
747 return;
749 # NOTE: we used to check whether we had a statement handle cached in
750 # a %handles module-level variable. This was a dumb move and
751 # broke things for the rest of us. prepare_cached is a better
752 # way to cache statement handles anyway.
753 my $query =
754 ($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
755 ($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
756 ($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
757 ($table eq 'issues' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
758 ($table eq 'old_issues' ) ? "SELECT * FROM $table WHERE itemnumber = ? ORDER BY timestamp DESC LIMIT 1" :
759 ($table eq 'reserves' ) ? "SELECT * FROM $table WHERE borrowernumber = ? and biblionumber = ?" :
760 ($table eq 'borrowers' ) ? "SELECT * FROM $table WHERE borrowernumber = ?" :
761 ($table eq 'branches' ) ? "SELECT * FROM $table WHERE branchcode = ?" :
762 ($table eq 'suggestions' ) ? "SELECT * FROM $table WHERE suggestionid = ?" :
763 ($table eq 'aqbooksellers') ? "SELECT * FROM $table WHERE id = ?" :
764 ($table eq 'aqorders' ) ? "SELECT * FROM $table WHERE ordernumber = ?" :
765 ($table eq 'opac_news' ) ? "SELECT * FROM $table WHERE idnew = ?" :
766 ($table eq 'article_requests') ? "SELECT * FROM $table WHERE id = ?" :
767 ($table eq 'borrower_modifications') ? "SELECT * FROM $table WHERE verification_token = ?" :
768 ($table eq 'subscription') ? "SELECT * FROM $table WHERE subscriptionid = ?" :
769 ($table eq 'serial') ? "SELECT * FROM $table WHERE serialid = ?" :
770 ($table eq 'problem_reports') ? "SELECT * FROM $table WHERE reportid = ?" :
771 undef ;
772 unless ($query) {
773 warn "ERROR: No _parseletter_sth query for table '$table'";
774 return; # nothing to get
776 unless ($sth = C4::Context->dbh->prepare_cached($query)) {
777 warn "ERROR: Failed to prepare query: '$query'";
778 return;
780 return $sth; # now cache is populated for that $table
783 =head2 _parseletter($letter, $table, $values)
785 parameters :
786 - $letter : a hash to letter fields (title & content useful)
787 - $table : the Koha table to parse.
788 - $values_in : table record hashref
789 parse all fields from a table, and replace values in title & content with the appropriate value
790 (not exported sub, used only internally)
792 =cut
794 sub _parseletter {
795 my ( $letter, $table, $values_in ) = @_;
797 # Work on a local copy of $values_in (passed by reference) to avoid side effects
798 # in callers ( by changing / formatting values )
799 my $values = $values_in ? { %$values_in } : {};
801 if ( $table eq 'borrowers' && $values->{'dateexpiry'} ){
802 $values->{'dateexpiry'} = output_pref({ dt => dt_from_string( $values->{'dateexpiry'} ), dateonly => 1 });
805 if ( $table eq 'reserves' && $values->{'waitingdate'} ) {
806 $values->{'waitingdate'} = output_pref({ dt => dt_from_string( $values->{'waitingdate'} ), dateonly => 1 });
809 if ($letter->{content} && $letter->{content} =~ /<<today>>/) {
810 my $todaysdate = output_pref( dt_from_string() );
811 $letter->{content} =~ s/<<today>>/$todaysdate/go;
814 while ( my ($field, $val) = each %$values ) {
815 $val =~ s/\p{P}$// if $val && $table=~/biblio/;
816 #BZ 9886: Assuming that we want to eliminate ISBD punctuation here
817 #Therefore adding the test on biblio. This includes biblioitems,
818 #but excludes items. Removed unneeded global and lookahead.
820 if ( $table=~/^borrowers$/ && $field=~/^streettype$/ ) {
821 my $av = Koha::AuthorisedValues->search({ category => 'ROADTYPE', authorised_value => $val });
822 $val = $av->count ? $av->next->lib : '';
825 # Dates replacement
826 my $replacedby = defined ($val) ? $val : '';
827 if ( $replacedby
828 and not $replacedby =~ m|0000-00-00|
829 and not $replacedby =~ m|9999-12-31|
830 and $replacedby =~ m|^\d{4}-\d{2}-\d{2}( \d{2}:\d{2}:\d{2})?$| )
832 # If the value is XXXX-YY-ZZ[ AA:BB:CC] we assume it is a date
833 my $dateonly = defined $1 ? 0 : 1; #$1 refers to the capture group wrapped in parentheses. In this case, that's the hours, minutes, seconds.
834 my $re_dateonly_filter = qr{ $field( \s* \| \s* dateonly\s*)?>> }xms;
836 for my $letter_field ( qw( title content ) ) {
837 my $filter_string_used = q{};
838 if ( $letter->{ $letter_field } =~ $re_dateonly_filter ) {
839 # We overwrite $dateonly if the filter exists and we have a time in the datetime
840 $filter_string_used = $1 || q{};
841 $dateonly = $1 unless $dateonly;
843 my $replacedby_date = eval {
844 output_pref({ dt => dt_from_string( $replacedby ), dateonly => $dateonly });
847 if ( $letter->{ $letter_field } ) {
848 $letter->{ $letter_field } =~ s/\Q<<$table.$field$filter_string_used>>\E/$replacedby_date/g;
849 $letter->{ $letter_field } =~ s/\Q<<$field$filter_string_used>>\E/$replacedby_date/g;
853 # Other fields replacement
854 else {
855 for my $letter_field ( qw( title content ) ) {
856 if ( $letter->{ $letter_field } ) {
857 $letter->{ $letter_field } =~ s/<<$table.$field>>/$replacedby/g;
858 $letter->{ $letter_field } =~ s/<<$field>>/$replacedby/g;
864 if ($table eq 'borrowers' && $letter->{content}) {
865 my $patron = Koha::Patrons->find( $values->{borrowernumber} );
866 if ( $patron ) {
867 my $attributes = $patron->extended_attributes;
868 my %attr;
869 while ( my $attribute = $attributes->next ) {
870 my $code = $attribute->code;
871 my $val = $attribute->description; # FIXME - we always display intranet description here!
872 $val =~ s/\p{P}(?=$)//g if $val;
873 next unless $val gt '';
874 $attr{$code} ||= [];
875 push @{ $attr{$code} }, $val;
877 while ( my ($code, $val_ar) = each %attr ) {
878 my $replacefield = "<<borrower-attribute:$code>>";
879 my $replacedby = join ',', @$val_ar;
880 $letter->{content} =~ s/$replacefield/$replacedby/g;
884 return $letter;
887 =head2 EnqueueLetter
889 my $success = EnqueueLetter( { letter => $letter,
890 borrowernumber => '12', message_transport_type => 'email' } )
892 places a letter in the message_queue database table, which will
893 eventually get processed (sent) by the process_message_queue.pl
894 cronjob when it calls SendQueuedMessages.
896 return message_id on success
898 =cut
900 sub EnqueueLetter {
901 my $params = shift or return;
903 return unless exists $params->{'letter'};
904 # return unless exists $params->{'borrowernumber'};
905 return unless exists $params->{'message_transport_type'};
907 my $content = $params->{letter}->{content};
908 $content =~ s/\s+//g if(defined $content);
909 if ( not defined $content or $content eq '' ) {
910 warn "Trying to add an empty message to the message queue" if $debug;
911 return;
914 # If we have any attachments we should encode then into the body.
915 if ( $params->{'attachments'} ) {
916 $params->{'letter'} = _add_attachments(
917 { letter => $params->{'letter'},
918 attachments => $params->{'attachments'},
919 message => MIME::Lite->new( Type => 'multipart/mixed' ),
924 my $dbh = C4::Context->dbh();
925 my $statement = << 'ENDSQL';
926 INSERT INTO message_queue
927 ( borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, to_address, from_address, reply_address, content_type )
928 VALUES
929 ( ?, ?, ?, ?, ?, ?, ?, NOW(), ?, ?, ?, ? )
930 ENDSQL
932 my $sth = $dbh->prepare($statement);
933 my $result = $sth->execute(
934 $params->{'borrowernumber'}, # borrowernumber
935 $params->{'letter'}->{'title'}, # subject
936 $params->{'letter'}->{'content'}, # content
937 $params->{'letter'}->{'metadata'} || '', # metadata
938 $params->{'letter'}->{'code'} || '', # letter_code
939 $params->{'message_transport_type'}, # message_transport_type
940 'pending', # status
941 $params->{'to_address'}, # to_address
942 $params->{'from_address'}, # from_address
943 $params->{'reply_address'}, # reply_address
944 $params->{'letter'}->{'content-type'}, # content_type
946 return $dbh->last_insert_id(undef,undef,'message_queue', undef);
949 =head2 SendQueuedMessages ([$hashref])
951 my $sent = SendQueuedMessages({
952 letter_code => $letter_code,
953 borrowernumber => $who_letter_is_for,
954 limit => 50,
955 verbose => 1,
956 type => 'sms',
959 Sends all of the 'pending' items in the message queue, unless
960 parameters are passed.
962 The letter_code, borrowernumber and limit parameters are used
963 to build a parameter set for _get_unsent_messages, thus limiting
964 which pending messages will be processed. They are all optional.
966 The verbose parameter can be used to generate debugging output.
967 It is also optional.
969 Returns number of messages sent.
971 =cut
973 sub SendQueuedMessages {
974 my $params = shift;
976 my $which_unsent_messages = {
977 'limit' => $params->{'limit'} // 0,
978 'borrowernumber' => $params->{'borrowernumber'} // q{},
979 'letter_code' => $params->{'letter_code'} // q{},
980 'type' => $params->{'type'} // q{},
982 my $unsent_messages = _get_unsent_messages( $which_unsent_messages );
983 MESSAGE: foreach my $message ( @$unsent_messages ) {
984 my $message_object = Koha::Notice::Messages->find( $message->{message_id} );
985 # If this fails the database is unwritable and we won't manage to send a message that continues to be marked 'pending'
986 $message_object->make_column_dirty('status');
987 return unless $message_object->store;
989 # warn Data::Dumper->Dump( [ $message ], [ 'message' ] );
990 warn sprintf( 'sending %s message to patron: %s',
991 $message->{'message_transport_type'},
992 $message->{'borrowernumber'} || 'Admin' )
993 if $params->{'verbose'} or $debug;
994 # This is just begging for subclassing
995 next MESSAGE if ( lc($message->{'message_transport_type'}) eq 'rss' );
996 if ( lc( $message->{'message_transport_type'} ) eq 'email' ) {
997 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
999 elsif ( lc( $message->{'message_transport_type'} ) eq 'sms' ) {
1000 if ( C4::Context->preference('SMSSendDriver') eq 'Email' ) {
1001 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1002 my $sms_provider = Koha::SMS::Providers->find( $patron->sms_provider_id );
1003 unless ( $sms_provider ) {
1004 warn sprintf( "Patron %s has no sms provider id set!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1005 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1006 next MESSAGE;
1008 unless ( $patron->smsalertnumber ) {
1009 _set_message_status( { message_id => $message->{'message_id'}, status => 'failed' } );
1010 warn sprintf( "No smsalertnumber found for patron %s!", $message->{'borrowernumber'} ) if $params->{'verbose'} or $debug;
1011 next MESSAGE;
1013 $message->{to_address} = $patron->smsalertnumber; #Sometime this is set to email - sms should always use smsalertnumber
1014 $message->{to_address} .= '@' . $sms_provider->domain();
1016 # Check for possible from_address override
1017 my $from_address = C4::Context->preference('EmailSMSSendDriverFromAddress');
1018 if ($from_address && $message->{from_address} ne $from_address) {
1019 $message->{from_address} = $from_address;
1020 _update_message_from_address($message->{'message_id'}, $message->{from_address});
1023 _update_message_to_address($message->{'message_id'}, $message->{to_address});
1024 _send_message_by_email( $message, $params->{'username'}, $params->{'password'}, $params->{'method'} );
1025 } else {
1026 _send_message_by_sms( $message );
1030 return scalar( @$unsent_messages );
1033 =head2 GetRSSMessages
1035 my $message_list = GetRSSMessages( { limit => 10, borrowernumber => '14' } )
1037 returns a listref of all queued RSS messages for a particular person.
1039 =cut
1041 sub GetRSSMessages {
1042 my $params = shift;
1044 return unless $params;
1045 return unless ref $params;
1046 return unless $params->{'borrowernumber'};
1048 return _get_unsent_messages( { message_transport_type => 'rss',
1049 limit => $params->{'limit'},
1050 borrowernumber => $params->{'borrowernumber'}, } );
1053 =head2 GetPrintMessages
1055 my $message_list = GetPrintMessages( { borrowernumber => $borrowernumber } )
1057 Returns a arrayref of all queued print messages (optionally, for a particular
1058 person).
1060 =cut
1062 sub GetPrintMessages {
1063 my $params = shift || {};
1065 return _get_unsent_messages( { message_transport_type => 'print',
1066 borrowernumber => $params->{'borrowernumber'},
1067 } );
1070 =head2 GetQueuedMessages ([$hashref])
1072 my $messages = GetQueuedMessage( { borrowernumber => '123', limit => 20 } );
1074 Fetches a list of messages from the message queue optionally filtered by borrowernumber
1075 and limited to specified limit.
1077 Return is an arrayref of hashes, each has represents a message in the message queue.
1079 =cut
1081 sub GetQueuedMessages {
1082 my $params = shift;
1084 my $dbh = C4::Context->dbh();
1085 my $statement = << 'ENDSQL';
1086 SELECT message_id, borrowernumber, subject, content, message_transport_type, status, time_queued, updated_on
1087 FROM message_queue
1088 ENDSQL
1090 my @query_params;
1091 my @whereclauses;
1092 if ( exists $params->{'borrowernumber'} ) {
1093 push @whereclauses, ' borrowernumber = ? ';
1094 push @query_params, $params->{'borrowernumber'};
1097 if ( @whereclauses ) {
1098 $statement .= ' WHERE ' . join( 'AND', @whereclauses );
1101 if ( defined $params->{'limit'} ) {
1102 $statement .= ' LIMIT ? ';
1103 push @query_params, $params->{'limit'};
1106 my $sth = $dbh->prepare( $statement );
1107 my $result = $sth->execute( @query_params );
1108 return $sth->fetchall_arrayref({});
1111 =head2 GetMessageTransportTypes
1113 my @mtt = GetMessageTransportTypes();
1115 returns an arrayref of transport types
1117 =cut
1119 sub GetMessageTransportTypes {
1120 my $dbh = C4::Context->dbh();
1121 my $mtts = $dbh->selectcol_arrayref("
1122 SELECT message_transport_type
1123 FROM message_transport_types
1124 ORDER BY message_transport_type
1126 return $mtts;
1129 =head2 GetMessage
1131 my $message = C4::Letters::Message($message_id);
1133 =cut
1135 sub GetMessage {
1136 my ( $message_id ) = @_;
1137 return unless $message_id;
1138 my $dbh = C4::Context->dbh;
1139 return $dbh->selectrow_hashref(q|
1140 SELECT message_id, borrowernumber, subject, content, metadata, letter_code, message_transport_type, status, time_queued, updated_on, to_address, from_address, reply_address, content_type
1141 FROM message_queue
1142 WHERE message_id = ?
1143 |, {}, $message_id );
1146 =head2 ResendMessage
1148 Attempt to resend a message which has failed previously.
1150 my $has_been_resent = C4::Letters::ResendMessage($message_id);
1152 Updates the message to 'pending' status so that
1153 it will be resent later on.
1155 returns 1 on success, 0 on failure, undef if no message was found
1157 =cut
1159 sub ResendMessage {
1160 my $message_id = shift;
1161 return unless $message_id;
1163 my $message = GetMessage( $message_id );
1164 return unless $message;
1165 my $rv = 0;
1166 if ( $message->{status} ne 'pending' ) {
1167 $rv = C4::Letters::_set_message_status({
1168 message_id => $message_id,
1169 status => 'pending',
1171 $rv = $rv > 0? 1: 0;
1172 # Clear destination email address to force address update
1173 _update_message_to_address( $message_id, undef ) if $rv &&
1174 $message->{message_transport_type} eq 'email';
1176 return $rv;
1179 =head2 _add_attachements
1181 named parameters:
1182 letter - the standard letter hashref
1183 attachments - listref of attachments. each attachment is a hashref of:
1184 type - the mime type, like 'text/plain'
1185 content - the actual attachment
1186 filename - the name of the attachment.
1187 message - a MIME::Lite object to attach these to.
1189 returns your letter object, with the content updated.
1191 =cut
1193 sub _add_attachments {
1194 my $params = shift;
1196 my $letter = $params->{'letter'};
1197 my $attachments = $params->{'attachments'};
1198 return $letter unless @$attachments;
1199 my $message = $params->{'message'};
1201 # First, we have to put the body in as the first attachment
1202 $message->attach(
1203 Type => $letter->{'content-type'} || 'TEXT',
1204 Data => $letter->{'is_html'}
1205 ? _wrap_html($letter->{'content'}, $letter->{'title'})
1206 : $letter->{'content'},
1209 foreach my $attachment ( @$attachments ) {
1210 $message->attach(
1211 Type => $attachment->{'type'},
1212 Data => $attachment->{'content'},
1213 Filename => $attachment->{'filename'},
1216 # we're forcing list context here to get the header, not the count back from grep.
1217 ( $letter->{'content-type'} ) = grep( /^Content-Type:/, split( /\n/, $params->{'message'}->header_as_string ) );
1218 $letter->{'content-type'} =~ s/^Content-Type:\s+//;
1219 $letter->{'content'} = $message->body_as_string;
1221 return $letter;
1225 =head2 _get_unsent_messages
1227 This function's parameter hash reference takes the following
1228 optional named parameters:
1229 message_transport_type: method of message sending (e.g. email, sms, etc.)
1230 borrowernumber : who the message is to be sent
1231 letter_code : type of message being sent (e.g. PASSWORD_RESET)
1232 limit : maximum number of messages to send
1234 This function returns an array of matching hash referenced rows from
1235 message_queue with some borrower information added.
1237 =cut
1239 sub _get_unsent_messages {
1240 my $params = shift;
1242 my $dbh = C4::Context->dbh();
1243 my $statement = qq{
1244 SELECT mq.message_id, mq.borrowernumber, mq.subject, mq.content, mq.message_transport_type, mq.status, mq.time_queued, mq.from_address, mq.reply_address, mq.to_address, mq.content_type, b.branchcode, mq.letter_code
1245 FROM message_queue mq
1246 LEFT JOIN borrowers b ON b.borrowernumber = mq.borrowernumber
1247 WHERE status = ?
1250 my @query_params = ('pending');
1251 if ( ref $params ) {
1252 if ( $params->{'message_transport_type'} ) {
1253 $statement .= ' AND mq.message_transport_type = ? ';
1254 push @query_params, $params->{'message_transport_type'};
1256 if ( $params->{'borrowernumber'} ) {
1257 $statement .= ' AND mq.borrowernumber = ? ';
1258 push @query_params, $params->{'borrowernumber'};
1260 if ( $params->{'letter_code'} ) {
1261 $statement .= ' AND mq.letter_code = ? ';
1262 push @query_params, $params->{'letter_code'};
1264 if ( $params->{'type'} ) {
1265 $statement .= ' AND message_transport_type = ? ';
1266 push @query_params, $params->{'type'};
1268 if ( $params->{'limit'} ) {
1269 $statement .= ' limit ? ';
1270 push @query_params, $params->{'limit'};
1274 $debug and warn "_get_unsent_messages SQL: $statement";
1275 $debug and warn "_get_unsent_messages params: " . join(',',@query_params);
1276 my $sth = $dbh->prepare( $statement );
1277 my $result = $sth->execute( @query_params );
1278 return $sth->fetchall_arrayref({});
1281 sub _send_message_by_email {
1282 my $message = shift or return;
1283 my ($username, $password, $method) = @_;
1285 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1286 my $to_address = $message->{'to_address'};
1287 unless ($to_address) {
1288 unless ($patron) {
1289 warn "FAIL: No 'to_address' and INVALID borrowernumber ($message->{borrowernumber})";
1290 _set_message_status( { message_id => $message->{'message_id'},
1291 status => 'failed' } );
1292 return;
1294 $to_address = $patron->notice_email_address;
1295 unless ($to_address) {
1296 # warn "FAIL: No 'to_address' and no email for " . ($member->{surname} ||'') . ", borrowernumber ($message->{borrowernumber})";
1297 # warning too verbose for this more common case?
1298 _set_message_status( { message_id => $message->{'message_id'},
1299 status => 'failed' } );
1300 return;
1304 # Encode subject line separately
1305 $message->{subject} = encode('MIME-Header', $message->{'subject'} );
1306 my $subject = $message->{'subject'};
1308 my $content = encode('UTF-8', $message->{'content'});
1309 my $content_type = $message->{'content_type'} || 'text/plain; charset="UTF-8"';
1310 my $is_html = $content_type =~ m/html/io;
1312 my $branch_email = undef;
1313 my $branch_replyto = undef;
1314 my $branch_returnpath = undef;
1315 my $library;
1317 if ($patron) {
1318 $library = $patron->library;
1319 $branch_email = $library->branchemail;
1320 $branch_replyto = $library->branchreplyto;
1321 $branch_returnpath = $library->branchreturnpath;
1324 my $email = Koha::Email->create(
1326 to => $to_address,
1328 C4::Context->preference('NoticeBcc')
1329 ? ( bcc => C4::Context->preference('NoticeBcc') )
1330 : ()
1332 from => $message->{'from_address'} || $branch_email,
1333 reply_to => $message->{'reply_address'} || $branch_replyto,
1334 sender => $branch_returnpath,
1335 subject => "" . $message->{subject}
1339 if ( $is_html ) {
1340 $email->html_body(
1341 _wrap_html( $content, $subject )
1344 else {
1345 $email->text_body( $content );
1348 my $smtp_server;
1349 if ( $library ) {
1350 $smtp_server = $library->smtp_server;
1352 else {
1353 $smtp_server = Koha::SMTP::Servers->get_default;
1356 if ( $username ) {
1357 $smtp_server->set(
1359 sasl_username => $username,
1360 sasl_password => $password,
1365 # if initial message address was empty, coming here means that a to address was found and
1366 # queue should be updated; same if to address was overriden by Koha::Email->create
1367 _update_message_to_address( $message->{'message_id'}, $email->email->header('To') )
1368 if !$message->{to_address}
1369 || $message->{to_address} ne $email->email->header('To');
1371 try {
1372 $email->send_or_die({ transport => $smtp_server->transport });
1374 _set_message_status(
1376 message_id => $message->{'message_id'},
1377 status => 'sent'
1380 return 1;
1382 catch {
1383 _set_message_status(
1385 message_id => $message->{'message_id'},
1386 status => 'failed'
1389 carp "$_";
1390 return;
1394 sub _wrap_html {
1395 my ($content, $title) = @_;
1397 my $css = C4::Context->preference("NoticeCSS") || '';
1398 $css = qq{<link rel="stylesheet" type="text/css" href="$css">} if $css;
1399 return <<EOS;
1400 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
1401 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1402 <html lang="en" xml:lang="en" xmlns="http://www.w3.org/1999/xhtml">
1403 <head>
1404 <title>$title</title>
1405 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
1406 $css
1407 </head>
1408 <body>
1409 $content
1410 </body>
1411 </html>
1415 sub _is_duplicate {
1416 my ( $message ) = @_;
1417 my $dbh = C4::Context->dbh;
1418 my $count = $dbh->selectrow_array(q|
1419 SELECT COUNT(*)
1420 FROM message_queue
1421 WHERE message_transport_type = ?
1422 AND borrowernumber = ?
1423 AND letter_code = ?
1424 AND CAST(updated_on AS date) = CAST(NOW() AS date)
1425 AND status="sent"
1426 AND content = ?
1427 |, {}, $message->{message_transport_type}, $message->{borrowernumber}, $message->{letter_code}, $message->{content} );
1428 return $count;
1431 sub _send_message_by_sms {
1432 my $message = shift or return;
1433 my $patron = Koha::Patrons->find( $message->{borrowernumber} );
1435 unless ( $patron and $patron->smsalertnumber ) {
1436 _set_message_status( { message_id => $message->{'message_id'},
1437 status => 'failed' } );
1438 return;
1441 if ( _is_duplicate( $message ) ) {
1442 _set_message_status( { message_id => $message->{'message_id'},
1443 status => 'failed' } );
1444 return;
1447 my $success = C4::SMS->send_sms( { destination => $patron->smsalertnumber,
1448 message => $message->{'content'},
1449 } );
1450 _set_message_status( { message_id => $message->{'message_id'},
1451 status => ($success ? 'sent' : 'failed') } );
1452 return $success;
1455 sub _update_message_to_address {
1456 my ($id, $to)= @_;
1457 my $dbh = C4::Context->dbh();
1458 $dbh->do('UPDATE message_queue SET to_address=? WHERE message_id=?',undef,($to,$id));
1461 sub _update_message_from_address {
1462 my ($message_id, $from_address) = @_;
1463 my $dbh = C4::Context->dbh();
1464 $dbh->do('UPDATE message_queue SET from_address = ? WHERE message_id = ?', undef, ($from_address, $message_id));
1467 sub _set_message_status {
1468 my $params = shift or return;
1470 foreach my $required_parameter ( qw( message_id status ) ) {
1471 return unless exists $params->{ $required_parameter };
1474 my $dbh = C4::Context->dbh();
1475 my $statement = 'UPDATE message_queue SET status= ? WHERE message_id = ?';
1476 my $sth = $dbh->prepare( $statement );
1477 my $result = $sth->execute( $params->{'status'},
1478 $params->{'message_id'} );
1479 return $result;
1482 sub _process_tt {
1483 my ( $params ) = @_;
1485 my $content = $params->{content};
1486 my $tables = $params->{tables};
1487 my $loops = $params->{loops};
1488 my $substitute = $params->{substitute} || {};
1490 my $use_template_cache = C4::Context->config('template_cache_dir') && defined $ENV{GATEWAY_INTERFACE};
1491 my $template = Template->new(
1493 EVAL_PERL => 1,
1494 ABSOLUTE => 1,
1495 PLUGIN_BASE => 'Koha::Template::Plugin',
1496 COMPILE_EXT => $use_template_cache ? '.ttc' : '',
1497 COMPILE_DIR => $use_template_cache ? C4::Context->config('template_cache_dir') : '',
1498 FILTERS => {},
1499 ENCODING => 'UTF-8',
1501 ) or die Template->error();
1503 my $tt_params = { %{ _get_tt_params( $tables ) }, %{ _get_tt_params( $loops, 'is_a_loop' ) }, %$substitute };
1505 $content = add_tt_filters( $content );
1506 $content = qq|[% USE KohaDates %][% USE Remove_MARC_punctuation %]$content|;
1508 my $output;
1509 $template->process( \$content, $tt_params, \$output ) || croak "ERROR PROCESSING TEMPLATE: " . $template->error();
1511 return $output;
1514 sub _get_tt_params {
1515 my ($tables, $is_a_loop) = @_;
1517 my $params;
1518 $is_a_loop ||= 0;
1520 my $config = {
1521 article_requests => {
1522 module => 'Koha::ArticleRequests',
1523 singular => 'article_request',
1524 plural => 'article_requests',
1525 pk => 'id',
1527 biblio => {
1528 module => 'Koha::Biblios',
1529 singular => 'biblio',
1530 plural => 'biblios',
1531 pk => 'biblionumber',
1533 biblioitems => {
1534 module => 'Koha::Biblioitems',
1535 singular => 'biblioitem',
1536 plural => 'biblioitems',
1537 pk => 'biblioitemnumber',
1539 borrowers => {
1540 module => 'Koha::Patrons',
1541 singular => 'borrower',
1542 plural => 'borrowers',
1543 pk => 'borrowernumber',
1545 branches => {
1546 module => 'Koha::Libraries',
1547 singular => 'branch',
1548 plural => 'branches',
1549 pk => 'branchcode',
1551 items => {
1552 module => 'Koha::Items',
1553 singular => 'item',
1554 plural => 'items',
1555 pk => 'itemnumber',
1557 opac_news => {
1558 module => 'Koha::News',
1559 singular => 'news',
1560 plural => 'news',
1561 pk => 'idnew',
1563 aqorders => {
1564 module => 'Koha::Acquisition::Orders',
1565 singular => 'order',
1566 plural => 'orders',
1567 pk => 'ordernumber',
1569 reserves => {
1570 module => 'Koha::Holds',
1571 singular => 'hold',
1572 plural => 'holds',
1573 pk => 'reserve_id',
1575 serial => {
1576 module => 'Koha::Serials',
1577 singular => 'serial',
1578 plural => 'serials',
1579 pk => 'serialid',
1581 subscription => {
1582 module => 'Koha::Subscriptions',
1583 singular => 'subscription',
1584 plural => 'subscriptions',
1585 pk => 'subscriptionid',
1587 suggestions => {
1588 module => 'Koha::Suggestions',
1589 singular => 'suggestion',
1590 plural => 'suggestions',
1591 pk => 'suggestionid',
1593 issues => {
1594 module => 'Koha::Checkouts',
1595 singular => 'checkout',
1596 plural => 'checkouts',
1597 fk => 'itemnumber',
1599 old_issues => {
1600 module => 'Koha::Old::Checkouts',
1601 singular => 'old_checkout',
1602 plural => 'old_checkouts',
1603 fk => 'itemnumber',
1605 overdues => {
1606 module => 'Koha::Checkouts',
1607 singular => 'overdue',
1608 plural => 'overdues',
1609 fk => 'itemnumber',
1611 borrower_modifications => {
1612 module => 'Koha::Patron::Modifications',
1613 singular => 'patron_modification',
1614 plural => 'patron_modifications',
1615 fk => 'verification_token',
1619 foreach my $table ( keys %$tables ) {
1620 next unless $config->{$table};
1622 my $ref = ref( $tables->{$table} ) || q{};
1623 my $module = $config->{$table}->{module};
1625 if ( can_load( modules => { $module => undef } ) ) {
1626 my $pk = $config->{$table}->{pk};
1627 my $fk = $config->{$table}->{fk};
1629 if ( $is_a_loop ) {
1630 my $values = $tables->{$table} || [];
1631 unless ( ref( $values ) eq 'ARRAY' ) {
1632 croak "ERROR processing table $table. Wrong API call.";
1634 my $key = $pk ? $pk : $fk;
1635 # $key does not come from user input
1636 my $objects = $module->search(
1637 { $key => $values },
1639 # We want to retrieve the data in the same order
1640 # FIXME MySQLism
1641 # field is a MySQLism, but they are no other way to do it
1642 # To be generic we could do it in perl, but we will need to fetch
1643 # all the data then order them
1644 @$values ? ( order_by => \[ "field($key, " . join( ', ', @$values ) . ")" ] ) : ()
1647 $params->{ $config->{$table}->{plural} } = $objects;
1649 elsif ( $ref eq q{} || $ref eq 'HASH' ) {
1650 my $id = ref $ref eq 'HASH' ? $tables->{$table}->{$pk} : $tables->{$table};
1651 my $object;
1652 if ( $fk ) { # Using a foreign key for lookup
1653 if ( ref( $fk ) eq 'ARRAY' ) { # Foreign key is multi-column
1654 my $search;
1655 foreach my $key ( @$fk ) {
1656 $search->{$key} = $id->{$key};
1658 $object = $module->search( $search )->last();
1659 } else { # Foreign key is single column
1660 $object = $module->search( { $fk => $id } )->last();
1662 } else { # using the table's primary key for lookup
1663 $object = $module->find($id);
1665 $params->{ $config->{$table}->{singular} } = $object;
1667 else { # $ref eq 'ARRAY'
1668 my $object;
1669 if ( @{ $tables->{$table} } == 1 ) { # Param is a single key
1670 $object = $module->search( { $pk => $tables->{$table} } )->last();
1672 else { # Params are mutliple foreign keys
1673 croak "Multiple foreign keys (table $table) should be passed using an hashref";
1675 $params->{ $config->{$table}->{singular} } = $object;
1678 else {
1679 croak "ERROR LOADING MODULE $module: $Module::Load::Conditional::ERROR";
1683 $params->{today} = output_pref({ dt => dt_from_string, dateformat => 'iso' });
1685 return $params;
1688 =head3 add_tt_filters
1690 $content = add_tt_filters( $content );
1692 Add TT filters to some specific fields if needed.
1694 For now we only add the Remove_MARC_punctuation TT filter to biblio and biblioitem fields
1696 =cut
1698 sub add_tt_filters {
1699 my ( $content ) = @_;
1700 $content =~ s|\[%\s*biblio\.(.*?)\s*%\]|[% biblio.$1 \| \$Remove_MARC_punctuation %]|gxms;
1701 $content =~ s|\[%\s*biblioitem\.(.*?)\s*%\]|[% biblioitem.$1 \| \$Remove_MARC_punctuation %]|gxms;
1702 return $content;
1705 =head2 get_item_content
1707 my $item = Koha::Items->find(...)->unblessed;
1708 my @item_content_fields = qw( date_due title barcode author itemnumber );
1709 my $item_content = C4::Letters::get_item_content({
1710 item => $item,
1711 item_content_fields => \@item_content_fields
1714 This function generates a tab-separated list of values for the passed item. Dates
1715 are formatted following the current setup.
1717 =cut
1719 sub get_item_content {
1720 my ( $params ) = @_;
1721 my $item = $params->{item};
1722 my $dateonly = $params->{dateonly} || 0;
1723 my $item_content_fields = $params->{item_content_fields} || [];
1725 return unless $item;
1727 my @item_info = map {
1728 $_ =~ /^date|date$/
1729 ? eval {
1730 output_pref(
1731 { dt => dt_from_string( $item->{$_} ), dateonly => $dateonly } );
1733 : $item->{$_}
1734 || ''
1735 } @$item_content_fields;
1736 return join( "\t", @item_info ) . "\n";
1740 __END__