From 24ab7ec0bfb052edce7a3c7a6e37c151f9cd6a04 Mon Sep 17 00:00:00 2001 From: Moritz Bunkus Date: Thu, 24 Sep 2015 10:04:44 +0200 Subject: [PATCH] E-Mail-Journal: verschickte E-Mails speichern --- SL/DB/EmailJournal.pm | 18 ++++++ SL/DB/EmailJournalAttachment.pm | 11 ++++ SL/DB/Helper/ALL.pm | 2 + SL/DB/Helper/Mappings.pm | 2 + SL/DB/Manager/EmailJournal.pm | 11 ++++ SL/DB/Manager/EmailJournalAttachment.pm | 14 +++++ SL/DB/MetaSetup/EmailJournal.pm | 38 +++++++++++ SL/DB/MetaSetup/EmailJournalAttachment.pm | 34 ++++++++++ SL/Mailer.pm | 77 +++++++++++++++++++---- SL/Mailer/SMTP.pm | 44 +++++++++---- SL/Mailer/Sendmail.pm | 19 ++++-- sql/Pg-upgrade2/email_journal.sql | 41 ++++++++++++ 12 files changed, 284 insertions(+), 27 deletions(-) create mode 100644 SL/DB/EmailJournal.pm create mode 100644 SL/DB/EmailJournalAttachment.pm create mode 100644 SL/DB/Manager/EmailJournal.pm create mode 100644 SL/DB/Manager/EmailJournalAttachment.pm create mode 100644 SL/DB/MetaSetup/EmailJournal.pm create mode 100644 SL/DB/MetaSetup/EmailJournalAttachment.pm create mode 100644 sql/Pg-upgrade2/email_journal.sql diff --git a/SL/DB/EmailJournal.pm b/SL/DB/EmailJournal.pm new file mode 100644 index 000000000..6875b5249 --- /dev/null +++ b/SL/DB/EmailJournal.pm @@ -0,0 +1,18 @@ +package SL::DB::EmailJournal; + +use strict; + +use SL::DB::MetaSetup::EmailJournal; +use SL::DB::Manager::EmailJournal; + +__PACKAGE__->meta->add_relationship( + attachments => { + type => 'one to many', + class => 'SL::DB::EmailJournalAttachment', + column_map => { id => 'email_journal_id' }, + }, +); + +__PACKAGE__->meta->initialize; + +1; diff --git a/SL/DB/EmailJournalAttachment.pm b/SL/DB/EmailJournalAttachment.pm new file mode 100644 index 000000000..0627e83d5 --- /dev/null +++ b/SL/DB/EmailJournalAttachment.pm @@ -0,0 +1,11 @@ +package SL::DB::EmailJournalAttachment; + +use strict; + +use SL::DB::MetaSetup::EmailJournalAttachment; +use SL::DB::Manager::EmailJournalAttachment; +use SL::DB::Helper::ActsAsList (group_by => [ qw(email_journal_id) ]); + +__PACKAGE__->meta->initialize; + +1; diff --git a/SL/DB/Helper/ALL.pm b/SL/DB/Helper/ALL.pm index fb9e833ca..3ab29433b 100644 --- a/SL/DB/Helper/ALL.pm +++ b/SL/DB/Helper/ALL.pm @@ -42,6 +42,8 @@ use SL::DB::Department; use SL::DB::Draft; use SL::DB::Dunning; use SL::DB::DunningConfig; +use SL::DB::EmailJournal; +use SL::DB::EmailJournalAttachment; use SL::DB::Employee; use SL::DB::Exchangerate; use SL::DB::Finanzamt; diff --git a/SL/DB/Helper/Mappings.pm b/SL/DB/Helper/Mappings.pm index 709e881ef..aa25bd449 100644 --- a/SL/DB/Helper/Mappings.pm +++ b/SL/DB/Helper/Mappings.pm @@ -126,6 +126,8 @@ my %kivitendo_package_names = ( drafts => 'draft', dunning => 'dunning', dunning_config => 'dunning_config', + email_journal => 'EmailJournal', + email_journal_attachments => 'EmailJournalAttachment', employee => 'employee', exchangerate => 'exchangerate', finanzamt => 'finanzamt', diff --git a/SL/DB/Manager/EmailJournal.pm b/SL/DB/Manager/EmailJournal.pm new file mode 100644 index 000000000..d387a7bab --- /dev/null +++ b/SL/DB/Manager/EmailJournal.pm @@ -0,0 +1,11 @@ +package SL::DB::Manager::EmailJournal; + +use strict; + +use parent qw(SL::DB::Helper::Manager); + +sub object_class { 'SL::DB::EmailJournal' } + +__PACKAGE__->make_manager_methods; + +1; diff --git a/SL/DB/Manager/EmailJournalAttachment.pm b/SL/DB/Manager/EmailJournalAttachment.pm new file mode 100644 index 000000000..fc7f2b1a3 --- /dev/null +++ b/SL/DB/Manager/EmailJournalAttachment.pm @@ -0,0 +1,14 @@ +# This file has been auto-generated only because it didn't exist. +# Feel free to modify it at will; it will not be overwritten automatically. + +package SL::DB::Manager::EmailJournalAttachment; + +use strict; + +use parent qw(SL::DB::Helper::Manager); + +sub object_class { 'SL::DB::EmailJournalAttachment' } + +__PACKAGE__->make_manager_methods; + +1; diff --git a/SL/DB/MetaSetup/EmailJournal.pm b/SL/DB/MetaSetup/EmailJournal.pm new file mode 100644 index 000000000..92f89be2d --- /dev/null +++ b/SL/DB/MetaSetup/EmailJournal.pm @@ -0,0 +1,38 @@ +# This file has been auto-generated. Do not modify it; it will be overwritten +# by rose_auto_create_model.pl automatically. +package SL::DB::EmailJournal; + +use strict; + +use parent qw(SL::DB::Object); + +__PACKAGE__->meta->table('email_journal'); + +__PACKAGE__->meta->columns( + body => { type => 'text', not_null => 1 }, + extended_status => { type => 'text', not_null => 1 }, + from => { type => 'text', not_null => 1 }, + headers => { type => 'text', not_null => 1 }, + id => { type => 'integer', not_null => 1, sequence => 'email_journal_id_seq1' }, + itime => { type => 'timestamp', default => 'now()', not_null => 1 }, + mtime => { type => 'timestamp', default => 'now()', not_null => 1 }, + recipients => { type => 'text', not_null => 1 }, + sender_id => { type => 'integer' }, + sent_on => { type => 'timestamp', default => 'now()', not_null => 1 }, + status => { type => 'text', not_null => 1 }, + subject => { type => 'text', not_null => 1 }, +); + +__PACKAGE__->meta->primary_key_columns([ 'id' ]); + +__PACKAGE__->meta->allow_inline_column_values(1); + +__PACKAGE__->meta->foreign_keys( + sender => { + class => 'SL::DB::Employee', + key_columns => { sender_id => 'id' }, + }, +); + +1; +; diff --git a/SL/DB/MetaSetup/EmailJournalAttachment.pm b/SL/DB/MetaSetup/EmailJournalAttachment.pm new file mode 100644 index 000000000..e804477cb --- /dev/null +++ b/SL/DB/MetaSetup/EmailJournalAttachment.pm @@ -0,0 +1,34 @@ +# This file has been auto-generated. Do not modify it; it will be overwritten +# by rose_auto_create_model.pl automatically. +package SL::DB::EmailJournalAttachment; + +use strict; + +use parent qw(SL::DB::Object); + +__PACKAGE__->meta->table('email_journal_attachments'); + +__PACKAGE__->meta->columns( + content => { type => 'bytea', not_null => 1 }, + email_journal_id => { type => 'integer', not_null => 1 }, + id => { type => 'serial', not_null => 1 }, + itime => { type => 'timestamp', default => 'now()', not_null => 1 }, + mime_type => { type => 'text', not_null => 1 }, + mtime => { type => 'timestamp', default => 'now()', not_null => 1 }, + name => { type => 'text', not_null => 1 }, + position => { type => 'integer', not_null => 1 }, +); + +__PACKAGE__->meta->primary_key_columns([ 'id' ]); + +__PACKAGE__->meta->allow_inline_column_values(1); + +__PACKAGE__->meta->foreign_keys( + email_journal => { + class => 'SL::DB::EmailJournal', + key_columns => { email_journal_id => 'id' }, + }, +); + +1; +; diff --git a/SL/Mailer.pm b/SL/Mailer.pm index 2f2408119..67f5fdd2f 100644 --- a/SL/Mailer.pm +++ b/SL/Mailer.pm @@ -25,8 +25,12 @@ package Mailer; use Email::Address; use Email::MIME::Creator; use File::Slurp; +use List::UtilsBy qw(bundle_by); use SL::Common; +use SL::DB::EmailJournal; +use SL::DB::EmailJournalAttachment; +use SL::DB::Employee; use SL::MIME; use SL::Template; @@ -185,7 +189,7 @@ sub send { # Create driver for delivery method (sendmail/SMTP) $self->{driver} = eval { $self->_create_driver }; if (!$self->{driver}) { - $::lxdebug->leave_sub(); + $self->_store_in_journal('failed', 'driver could not be created; check your configuration'); return "send email : $@"; } @@ -198,20 +202,71 @@ sub send { 'X-Mailer' => "kivitendo $self->{version}", ]; - # Clean up To/Cc/Bcc address fields - $self->_cleanup_addresses; - $self->_create_address_headers; + my $error; + my $ok = eval { + # Clean up To/Cc/Bcc address fields + $self->_cleanup_addresses; + $self->_create_address_headers; - my $email = $self->_create_message; + my $email = $self->_create_message; - # $::lxdebug->message(0, "message: " . $email->as_string); - # return "boom"; + # $::lxdebug->message(0, "message: " . $email->as_string); + # return "boom"; - $self->{driver}->start_mail(from => $self->{from}, to => [ map { @{ $self->{addresses}->{$_} } } qw(to cc bcc) ]); - $self->{driver}->print($email->as_string); - $self->{driver}->send; + $self->{driver}->start_mail(from => $self->{from}, to => [ $self->_all_recipients ]); + $self->{driver}->print($email->as_string); + $self->{driver}->send; - return ''; + 1; + }; + + $error = $@ if !$ok; + + $self->_store_in_journal; + + return $ok ? '' : "send email: $error"; +} + +sub _all_recipients { + my ($self) = @_; + + $self->{addresses} ||= {}; + return map { @{ $self->{addresses}->{$_} || [] } } qw(to cc bcc); +} + +sub _store_in_journal { + my ($self, $status, $extended_status) = @_; + + $status //= $self->{driver}->status if $self->{driver}; + $status //= 'failed'; + $extended_status //= $self->{driver}->extended_status if $self->{driver}; + $extended_status //= 'unknown error'; + + my @attachments = grep { $_ } map { + my $part = $self->_create_attachment_part($_); + if ($part) { + SL::DB::EmailJournalAttachment->new( + name => $part->filename, + mime_type => $part->content_type, + content => $part->body, + ) + } + } @{ $self->{attachments} || [] }; + + my $headers = join "\r\n", (bundle_by { join(': ', @_) } 2, @{ $self->{headers} || [] }); + + SL::DB::EmailJournal->new( + sender => SL::DB::Manager::Employee->current, + from => $self->{from} // '', + recipients => join(', ', $self->_all_recipients), + subject => $self->{subject} // '', + headers => $headers, + body => $self->{message} // '', + sent_on => DateTime->now_local, + attachments => \@attachments, + status => $status, + extended_status => $extended_status, + )->save; } 1; diff --git a/SL/Mailer/SMTP.pm b/SL/Mailer/SMTP.pm index 38ad2b753..ce5439722 100644 --- a/SL/Mailer/SMTP.pm +++ b/SL/Mailer/SMTP.pm @@ -6,7 +6,7 @@ use parent qw(Rose::Object); use Rose::Object::MakeMethods::Generic ( - scalar => [ qw(myconfig mailer form) ] + scalar => [ qw(myconfig mailer form status extended_status) ] ); my %security_config = ( @@ -18,18 +18,33 @@ my %security_config = ( sub init { my ($self) = @_; - Rose::Object::init(@_); + Rose::Object::init( + @_, + status => 'failed', + extended_status => 'no send attempt made', + ); my $cfg = $::lx_office_conf{mail_delivery} || {}; $self->{security} = exists $security_config{lc $cfg->{security}} ? lc $cfg->{security} : 'none'; my $sec_cfg = $security_config{ $self->{security} }; - eval "require $sec_cfg->{require_module}" or die "$@"; + eval "require $sec_cfg->{require_module}" or do { + $self->extended_status("$@"); + die $self->extended_status; + }; $self->{smtp} = $sec_cfg->{package}->new($cfg->{host} || 'localhost', Port => $cfg->{port} || $sec_cfg->{port}); - die unless $self->{smtp}; - - $self->{smtp}->starttls(SSL_verify_mode => 0) || die if $self->{security} eq 'tls'; + if (!$self->{smtp}) { + $self->extended_status('SMTP connection could not be initialized'); + die $self->extended_status; + } + + if ($self->{security} eq 'tls') { + $self->{smtp}->starttls(SSL_verify_mode => 0) or do { + $self->extended_status("$@"); + die $self->extended_status; + }; + } # Backwards compatibility: older Versions used 'user' instead of the # intended 'login'. Support both. @@ -37,15 +52,18 @@ sub init { return 1 unless $login; - $self->{smtp}->auth($login, $cfg->{password}) or die; + if (!$self->{smtp}->auth($login, $cfg->{password})) { + $self->extended_status('SMTP authentication failed'); + die $self->extended_status; + } } sub start_mail { my ($self, %params) = @_; - $self->{smtp}->mail($params{from}); - $self->{smtp}->recipient(@{ $params{to} }); - $self->{smtp}->data; + $self->{smtp}->mail($params{from}) or do { $self->extended_status($self->{smtp}->message); die $self->extended_status; }; + $self->{smtp}->recipient(@{ $params{to} }) or do { $self->extended_status($self->{smtp}->message); die $self->extended_status; }; + $self->{smtp}->data or do { $self->extended_status($self->{smtp}->message); die $self->extended_status; }; } sub print { @@ -76,8 +94,12 @@ sub print { sub send { my ($self) = @_; - $self->{smtp}->dataend; + my $ok = $self->{smtp}->dataend; + $self->extended_status($self->{smtp}->message); + $self->status('ok') if $ok; + $self->{smtp}->quit; + delete $self->{smtp}; } diff --git a/SL/Mailer/Sendmail.pm b/SL/Mailer/Sendmail.pm index 3a6c2c6ca..59c43cdd1 100644 --- a/SL/Mailer/Sendmail.pm +++ b/SL/Mailer/Sendmail.pm @@ -10,13 +10,17 @@ use parent qw(Rose::Object); use Rose::Object::MakeMethods::Generic ( - scalar => [ qw(myconfig mailer form) ] + scalar => [ qw(myconfig mailer form status extended_status) ] ); sub init { my ($self) = @_; - Rose::Object::init(@_); + Rose::Object::init( + @_, + status => 'failed', + extended_status => 'no send attempt made', + ); my $email = Encode::encode('utf-8', $self->myconfig->{email}); $email =~ s/[^\w\.\-\+=@]//ig; @@ -26,7 +30,7 @@ sub init { my $sendmail = $::lx_office_conf{applications}->{sendmail} || $::lx_office_conf{mail_delivery}->{sendmail} || "sendmail -t"; $sendmail = $template->parse_block($sendmail); - $self->{sendmail} = IO::File->new("|$sendmail") || die "sendmail($sendmail): $!"; + $self->{sendmail} = IO::File->new("|$sendmail") or do { $self->extended_status("sendmail($sendmail): $!"); die $self->extended_status; }; $self->{sendmail}->binmode(':utf8'); } @@ -36,12 +40,17 @@ sub start_mail { sub print { my $self = shift; - $self->{sendmail}->print(@_); + $self->{sendmail}->print(@_) or do { $self->extended_status("sendmail: $!"); die $self->extended_status; }; } sub send { my ($self) = @_; - $self->{sendmail}->close; + + $self->{sendmail}->close or do { $self->extended_status("sendmail: $!"); die $self->extended_status; }; + + $self->status('ok'); + $self->extended_status(''); + delete $self->{sendmail}; } diff --git a/sql/Pg-upgrade2/email_journal.sql b/sql/Pg-upgrade2/email_journal.sql new file mode 100644 index 000000000..24b1b9afd --- /dev/null +++ b/sql/Pg-upgrade2/email_journal.sql @@ -0,0 +1,41 @@ +-- @tag: email_journal +-- @description: Journal für verschickte E-Mails +-- @depends: release_3_3_0 + +-- Note: sender_id may be NULL to indicate a mail sent by the system +-- without a user being logged in – e.g. by the task server. +CREATE TABLE email_journal ( + id SERIAL NOT NULL, + sender_id INTEGER, + "from" TEXT NOT NULL, + recipients TEXT NOT NULL, + sent_on TIMESTAMP NOT NULL DEFAULT now(), + subject TEXT NOT NULL, + body TEXT NOT NULL, + headers TEXT NOT NULL, + status TEXT NOT NULL, + extended_status TEXT NOT NULL, + itime TIMESTAMP NOT NULL DEFAULT now(), + mtime TIMESTAMP NOT NULL DEFAULT now(), + + PRIMARY KEY (id), + FOREIGN KEY (sender_id) REFERENCES employee (id), + CONSTRAINT valid_status CHECK (status IN ('ok', 'failed')) +); + +CREATE TABLE email_journal_attachments ( + id SERIAL NOT NULL, + position INTEGER NOT NULL, + email_journal_id INTEGER NOT NULL, + name TEXT NOT NULL, + mime_type TEXT NOT NULL, + content BYTEA NOT NULL, + itime TIMESTAMP NOT NULL DEFAULT now(), + mtime TIMESTAMP NOT NULL DEFAULT now(), + + PRIMARY KEY (id), + FOREIGN KEY (email_journal_id) REFERENCES email_journal (id) ON DELETE CASCADE +); + +CREATE TRIGGER mtime_email_journal BEFORE UPDATE ON email_journal FOR EACH ROW EXECUTE PROCEDURE set_mtime(); +CREATE TRIGGER mtime_email_journal_attachments BEFORE UPDATE ON email_journal_attachments FOR EACH ROW EXECUTE PROCEDURE set_mtime(); -- 2.20.1