E-Mail-Journal: verschickte E-Mails speichern
authorMoritz Bunkus <m.bunkus@linet-services.de>
Thu, 24 Sep 2015 08:04:44 +0000 (10:04 +0200)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Thu, 24 Sep 2015 09:43:22 +0000 (11:43 +0200)
12 files changed:
SL/DB/EmailJournal.pm [new file with mode: 0644]
SL/DB/EmailJournalAttachment.pm [new file with mode: 0644]
SL/DB/Helper/ALL.pm
SL/DB/Helper/Mappings.pm
SL/DB/Manager/EmailJournal.pm [new file with mode: 0644]
SL/DB/Manager/EmailJournalAttachment.pm [new file with mode: 0644]
SL/DB/MetaSetup/EmailJournal.pm [new file with mode: 0644]
SL/DB/MetaSetup/EmailJournalAttachment.pm [new file with mode: 0644]
SL/Mailer.pm
SL/Mailer/SMTP.pm
SL/Mailer/Sendmail.pm
sql/Pg-upgrade2/email_journal.sql [new file with mode: 0644]

diff --git a/SL/DB/EmailJournal.pm b/SL/DB/EmailJournal.pm
new file mode 100644 (file)
index 0000000..6875b52
--- /dev/null
@@ -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 (file)
index 0000000..0627e83
--- /dev/null
@@ -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;
index fb9e833..3ab2943 100644 (file)
@@ -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;
index 709e881..aa25bd4 100644 (file)
@@ -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 (file)
index 0000000..d387a7b
--- /dev/null
@@ -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 (file)
index 0000000..fc7f2b1
--- /dev/null
@@ -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 (file)
index 0000000..92f89be
--- /dev/null
@@ -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 (file)
index 0000000..e804477
--- /dev/null
@@ -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;
+;
index 2f24081..67f5fdd 100644 (file)
@@ -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;
index 38ad2b7..ce54397 100644 (file)
@@ -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};
 }
 
index 3a6c2c6..59c43cd 100644 (file)
@@ -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 (file)
index 0000000..24b1b9a
--- /dev/null
@@ -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();