]> wagnertech.de Git - mfinanz.git/blob - SL/Mailer.pm
restart apache2 in postinst
[mfinanz.git] / SL / Mailer.pm
1 #=====================================================================
2 # LX-Office ERP
3 # Copyright (C) 2004
4 # Based on SQL-Ledger Version 2.1.9
5 # Web http://www.lx-office.org
6 #
7 #=====================================================================
8 #
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 2 of the License, or
12 # (at your option) any later version.
13 #
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
21 # MA 02110-1335, USA.
22 #======================================================================
23
24 package Mailer;
25
26 use IO::Socket::INET;
27 use IO::Socket::SSL;
28 use Email::Address;
29 use Email::MIME::Creator;
30 use Encode;
31 use File::MimeInfo::Magic;
32 use File::Slurp;
33 use List::UtilsBy qw(bundle_by);
34 use List::Util qw(sum);
35
36 use SL::Common;
37 use SL::DB::EmailJournal;
38 use SL::DB::EmailJournalAttachment;
39 use SL::DB::Employee;
40 use SL::Locale::String qw(t8);
41 use SL::Template;
42 use SL::Version;
43 use SL::IMAPClient;
44
45 use strict;
46
47 my $num_sent = 0;
48
49 my %mail_delivery_modules = (
50   sendmail => 'SL::Mailer::Sendmail',
51   smtp     => 'SL::Mailer::SMTP',
52 );
53
54 my %type_to_table = (
55   sales_quotation         => 'oe',
56   request_quotation       => 'oe',
57   sales_order             => 'oe',
58   purchase_order          => 'oe',
59   invoice                 => 'ar',
60   credit_note             => 'ar',
61   purchase_invoice        => 'ap',
62   letter                  => 'letter',
63   purchase_delivery_order => 'delivery_orders',
64   sales_delivery_order    => 'delivery_orders',
65   dunning                 => 'dunning',
66 );
67 my %type_to_email = (
68   sales_quotation         => sub { $::instance_conf->get_email_sender_sales_quotation         },
69   request_quotation       => sub { $::instance_conf->get_email_sender_request_quotation       },
70   sales_order             => sub { $::instance_conf->get_email_sender_sales_order             },
71   purchase_order          => sub { $::instance_conf->get_email_sender_purchase_order          },
72   invoice                 => sub { $::instance_conf->get_email_sender_invoice                 },
73   credit_note             => sub { $::instance_conf->get_email_sender_invoice                 },
74   purchase_invoice        => sub { $::instance_conf->get_email_sender_purchase_invoice        },
75   letter                  => sub { $::instance_conf->get_email_sender_letter                  },
76   purchase_delivery_order => sub { $::instance_conf->get_email_sender_purchase_delivery_order },
77   sales_delivery_order    => sub { $::instance_conf->get_email_sender_sales_delivery_order    },
78   dunning                 => sub { $::instance_conf->get_email_sender_dunning                 },
79 );
80
81
82 sub new {
83   my ($type, %params) = @_;
84   my $self = { %params };
85
86   bless $self, $type;
87 }
88
89 sub _create_driver {
90   my ($self) = @_;
91
92   my %params = (
93     mailer   => $self,
94     form     => $::form,
95     myconfig => \%::myconfig,
96   );
97
98   my $module = $mail_delivery_modules{ $::lx_office_conf{mail_delivery}->{method} };
99   eval "require $module" or return undef;
100
101   return $module->new(%params);
102 }
103
104 sub _cleanup_addresses {
105   my ($self) = @_;
106
107   foreach my $item (qw(to cc bcc)) {
108     next unless $self->{$item};
109
110     $self->{$item} =~ s/\&lt;/</g;
111     $self->{$item} =~ s/\$<\$/</g;
112     $self->{$item} =~ s/\&gt;/>/g;
113     $self->{$item} =~ s/\$>\$/>/g;
114   }
115 }
116
117 sub _create_message_id {
118   my ($self) = @_;
119
120   $num_sent  +=  1;
121   my $domain  =  $self->{from};
122   $domain     =~ s/.*\@//;
123   $domain     =~ s/>.*//;
124
125   return  "kivitendo-" . SL::Version->get_version . "-" . time() . "-${$}-${num_sent}\@$domain";
126 }
127
128 sub _create_address_headers {
129   my ($self) = @_;
130
131   # $self->{addresses} collects the recipients for use in e.g. the
132   # SMTP 'RCPT TO:' envelope command. $self->{headers} collects the
133   # headers that make up the actual email. 'BCC' should not be
134   # included there for certain transportation methods (SMTP).
135
136   $self->{addresses} = {};
137
138   foreach my $item (qw(from to cc bcc)) {
139     $self->{addresses}->{$item} = [];
140     next if !$self->{$item};
141
142     my @header_addresses;
143     my @addresses = Email::Address->parse($self->{$item});
144
145     # if either no address was parsed or
146     # there are more than 3 characters per parsed email extra, assume the the user has entered bunk
147     if (!@addresses) {
148        die t8('"#1" seems to be a faulty list of email addresses. No addresses could be extracted.',
149          $self->{$item},
150        );
151     } elsif ((length($self->{$item}) - sum map { length $_->original } @addresses) / @addresses > 3) {
152        die t8('"#1" seems to be a faulty list of email addresses. After extracing addresses (#2) too many characters are left.',
153          $self->{$item}, join ', ', map { $_->original } @addresses,
154        );
155     }
156
157     foreach my $addr_obj (@addresses) {
158       push @{ $self->{addresses}->{$item} }, $addr_obj->address;
159       next if $self->{driver}->keep_from_header($item);
160
161       my $phrase = $addr_obj->phrase();
162       if ($phrase) {
163         $phrase =~ s/^\"//;
164         $phrase =~ s/\"$//;
165         $addr_obj->phrase($phrase);
166       }
167
168       push @header_addresses, $addr_obj->format;
169     }
170
171     push @{ $self->{headers} }, ( ucfirst($item) => join(', ', @header_addresses) ) if @header_addresses;
172   }
173 }
174
175 sub _create_attachment_part {
176   my ($self, $attachment) = @_;
177
178   my %attributes = (
179     disposition  => 'attachment',
180     encoding     => 'base64',
181   );
182
183   my $attachment_content;
184   my $file_id       = 0;
185   my $email_journal = $::instance_conf->get_email_journal;
186
187   if (ref($attachment) eq "HASH") {
188     $attributes{filename}     = $attachment->{name};
189     $file_id                  = $attachment->{id}   || '0';
190     $attributes{content_type} = $attachment->{type} || 'application/pdf';
191     $attachment_content       = $attachment->{content};
192     $attachment_content       = eval { read_file($attachment->{path}) } if !$attachment_content;
193
194   } else {
195     $attributes{filename} =  $attachment;
196     $attributes{filename} =~ s:.*\Q$self->{fileid}\E:: if $self->{fileid};
197     $attributes{filename} =~ s:.*/::g;
198
199     my $application             = ($attachment =~ /(^\w+$)|\.(html|text|txt|sql)$/) ? 'text' : 'application';
200     $attributes{content_type}   = File::MimeInfo::Magic::magic($attachment);
201     $attributes{content_type} ||= "${application}/$self->{format}" if $self->{format};
202     $attributes{content_type} ||= 'application/octet-stream';
203     $attachment_content         = eval { read_file($attachment) };
204   }
205
206   return undef if $email_journal > 1 && !defined $attachment_content;
207
208   $attachment_content ||= ' ';
209   $attributes{charset}  = $self->{charset} if $self->{charset} && ($attributes{content_type} =~ m{^text/});
210
211   my $ent;
212   if ( $attributes{content_type} eq 'message/rfc822' ) {
213     $ent = Email::MIME->new($attachment_content);
214   } else {
215     $ent = Email::MIME->create(
216       attributes => \%attributes,
217       body       => $attachment_content,
218     );
219   }
220
221   # Due to a bug in Email::MIME it's not enough to hand over the encoded file name in the "attributes" hash in the
222   # "create" call. Email::MIME iterates over the keys in the hash, and depending on which key it has already seen during
223   # the iteration it might revert the encoding. As Perl's hash key order is randomized for each Perl run, this means
224   # that the file name stays unencoded sometimes.
225   # Setting the header manually after the "create" call circumvents this problem.
226   $ent->header_set('Content-disposition' => 'attachment; filename="' . encode('MIME-Q', $attributes{filename}) . '"');
227
228   push @{ $self->{mail_attachments}} , SL::DB::EmailJournalAttachment->new(
229     name      => $attributes{filename},
230     mime_type => $attributes{content_type},
231     content   => ( $email_journal > 1 ? $attachment_content : ' '),
232     file_id   => $file_id,
233   );
234
235   return $ent;
236 }
237
238 sub _create_message {
239   my ($self) = @_;
240
241   my @parts;
242
243   if ($self->{message}) {
244     push @parts, Email::MIME->create(
245       attributes => {
246         content_type => $self->{content_type},
247         charset      => $self->{charset},
248         encoding     => 'quoted-printable',
249       },
250       body_str => $self->{message},
251     );
252
253     push @{ $self->{headers} }, (
254       'Content-Type' => qq|$self->{content_type}; charset="$self->{charset}"|,
255     );
256   }
257
258   push @parts, grep { $_ } map { $self->_create_attachment_part($_) } @{ $self->{attachments} || [] };
259
260   return Email::MIME->create(
261       header_str => $self->{headers},
262       parts      => \@parts,
263   );
264 }
265
266 sub send {
267   my ($self) = @_;
268
269   # Create driver for delivery method (sendmail/SMTP)
270   $self->{driver} = eval { $self->_create_driver };
271   if (!$self->{driver}) {
272     my $error = $@;
273     $self->_store_in_journal('send_failed', 'driver could not be created; check your configuration & log files');
274     $::lxdebug->message(LXDebug::WARN(), "Mailer error during 'send': $error");
275
276     return $error;
277   }
278   $self->_default_from;  # set from for records if configured in client config
279   # Set defaults & headers
280   $self->{charset}        =  'UTF-8';
281   $self->{content_type} ||=  "text/plain";
282   $self->{headers}      ||=  [];
283   push @{ $self->{headers} }, (
284     Subject               => $self->{subject},
285     'Message-ID'          => '<' . $self->_create_message_id . '>',
286     'X-Mailer'            => "kivitendo " . SL::Version->get_version,
287   );
288   $self->{mail_attachments} = [];
289
290   my $email_as_string;
291   my $error;
292   my $ok = eval {
293     # Clean up To/Cc/Bcc address fields
294     $self->_cleanup_addresses;
295     $self->_create_address_headers;
296
297     my $email = $self->_create_message;
298
299     my $from_obj = (Email::Address->parse($self->{from}))[0];
300
301     $self->{driver}->start_mail(from => $from_obj->address, to => [ $self->_all_recipients ]);
302     $self->{driver}->print($email->as_string);
303     $self->{driver}->send;
304
305     $email_as_string = $email->as_string;
306     1;
307   };
308
309   $error = $@ if !$ok;
310
311   # TODO: Error is not for sending emial
312   # in SL::Form->send_email error is treated as error for sending email
313   if ($ok) {
314     eval {$self->_store_in_imap_sent_folder($email_as_string); 1} or do {
315       $ok = 0;
316       $error = $@;
317     };
318   }
319
320   # create journal and link to record
321   $self->{journalentry} = $self->_store_in_journal;
322   $self->_create_record_link if $self->{journalentry};
323
324   return $ok ? '' : ($error || "undefined error");
325 }
326
327 sub _all_recipients {
328   my ($self) = @_;
329   $self->{addresses} ||= {};
330   return map { @{ $self->{addresses}->{$_} || [] } } qw(to cc bcc);
331 }
332
333 sub _get_header_string {
334   my ($self) = @_;
335   my $header_string =
336     join "\r\n",
337       (bundle_by { join(': ', @_) } 2, @{ $self->{headers} || [] });
338   return $header_string;
339 }
340
341 sub _store_in_imap_sent_folder {
342   my ($self, $email_as_string) = @_;
343
344   my $from_email = $self->{from};
345   my $user_email = $::myconfig{email};
346   my $imap_config =
347        $::lx_office_conf{"sent_emails_in_imap/email/$from_email"}
348     || $::lx_office_conf{"sent_emails_in_imap/email/$user_email"}
349     || $::lx_office_conf{sent_emails_in_imap}
350     || {};
351   return unless ($imap_config->{enabled});
352
353   my $folder = delete $imap_config->{folder};
354   my $imap_client = SL::IMAPClient->new(%$imap_config);
355   $imap_client->store_email_in_email_folder(
356     email_as_string => $email_as_string,
357     folder => $folder ||'Sent/Kivitendo',
358   );
359
360   return 1;
361 }
362
363 sub _store_in_journal {
364   my ($self, $status, $extended_status) = @_;
365
366   my $journal_enable = $::instance_conf->get_email_journal;
367
368   return if $journal_enable == 0;
369
370   $status          //= $self->{driver}->status if $self->{driver};
371   $status          //= 'send_failed';
372   $extended_status //= $self->{driver}->extended_status if $self->{driver};
373   $extended_status //= 'unknown error';
374
375   my $headers = $self->_get_header_string;
376
377   my $jentry = SL::DB::EmailJournal->new(
378     sender          => SL::DB::Manager::Employee->current,
379     from            => $self->{from}    // '',
380     recipients      => join(', ', $self->_all_recipients),
381     subject         => $self->{subject} // '',
382     headers         => $headers,
383     body            => $self->{message} // '',
384     sent_on         => DateTime->now_local,
385     attachments     => \@{ $self->{mail_attachments} },
386     status          => $status,
387     extended_status => $extended_status,
388   )->save;
389   return $jentry->id;
390 }
391
392
393 sub _create_record_link {
394   my ($self) = @_;
395
396   # check for custom/overloaded types and ids (form != controller)
397   my $record_type = $self->{record_type} || $::form->{type} || '';
398   my $record_id   = $self->{record_id}   || $::form->{id};
399
400   # you may send mails for unsaved objects (no record_id => unlinkable case)
401   if ($self->{journalentry} && $record_id && exists($type_to_table{$record_type})) {
402     RecordLinks->create_links(
403       mode       => 'ids',
404       from_table => $type_to_table{$record_type},
405       from_ids   => $record_id,
406       to_table   => 'email_journal',
407       to_id      => $self->{journalentry},
408     );
409   }
410 }
411
412
413 sub _default_from {
414   my ($self) = @_;
415
416   my $record_type  = $self->{record_type} || $::form->{type} || $self->{driver}{form}{formname} || '';
417   my $record_email = exists $type_to_email{$record_type} ? $type_to_email{$record_type}->() : undef;
418   $self->{from}    = $record_email if $record_email;
419 }
420
421 1;
422
423
424 __END__
425
426 =pod
427
428 =encoding utf8
429
430 =head1 NAME
431
432 SL::Mailer - Base class for sending mails from kivitendo
433
434 =head1 SYNOPSIS
435
436   package SL::BackgroundJob::CreatePeriodicInvoices;
437
438   use SL::Mailer;
439
440   my $mail              = Mailer->new;
441   $mail->{from}         = $config{periodic_invoices}->{email_from};
442   $mail->{to}           = $email;
443   $mail->{subject}      = $config{periodic_invoices}->{email_subject};
444   $mail->{content_type} = $filename =~ m/.html$/ ? 'text/html' : 'text/plain';
445   $mail->{message}      = $output;
446
447   $mail->send;
448
449 =head1 OVERVIEW
450
451 Mail can be sent from kivitendo via the sendmail command or the smtp protocol.
452
453
454 =head1 INTERNAL DATA TYPES
455
456
457 =over 2
458
459 =item C<%mail_delivery_modules>
460
461   Currently two modules are supported: smtp or sendmail.
462
463 =item C<%type_to_table>
464
465   Due to the lack of a single global mapping for $form->{type},
466   type is mapped to the corresponding database table. All types which
467   implement a mail action are currently mapped and should be mapped.
468   Type is either the value of the old form or the newer controller
469   based object type.
470
471 =back
472
473 =head1 FUNCTIONS
474
475 =over 4
476
477 =item C<new>
478
479 =item C<_create_driver>
480
481 =item C<_cleanup_addresses>
482
483 =item C<_create_address_headers>
484
485 =item C<_create_message_id>
486
487 =item C<_create_attachment_part>
488
489 =item C<_create_message>
490
491 =item C<send>
492
493   If a mail was sent successfully the internal function _store_in_journal
494   is called if email journaling is enabled. If _store_in_journal was executed
495   successfully and the calling form is already persistent (database id) a
496   record_link will be created.
497
498 =item C<_all_recipients>
499
500 =item C<_store_in_journal>
501
502 =item C<_create_record_link $self->{journalentry}, $::form->{id}, $self->{record_id}>
503
504
505   If $self->{journalentry} and either $self->{record_id} or $::form->{id} (checked in
506   this order) exist a record link from record to email journal is created.
507   It is possible to provide an array reference with more than one id in
508   $self->{record_id} or $::form->{id}. In this case all records are linked to
509   the mail.
510   Will fail silently if record_link creation wasn't successful (same behaviour as
511   _store_in_journal).
512
513 =item C<validate>
514
515 =back
516
517 =head1 BUGS
518
519 Nothing here yet.
520
521 =head1 AUTHOR
522
523 =cut