--- /dev/null
+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;
--- /dev/null
+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;
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;
drafts => 'draft',
dunning => 'dunning',
dunning_config => 'dunning_config',
+ email_journal => 'EmailJournal',
+ email_journal_attachments => 'EmailJournalAttachment',
employee => 'employee',
exchangerate => 'exchangerate',
finanzamt => 'finanzamt',
--- /dev/null
+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;
--- /dev/null
+# 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;
--- /dev/null
+# 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;
+;
--- /dev/null
+# 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;
+;
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;
# 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 : $@";
}
'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;
use Rose::Object::MakeMethods::Generic
(
- scalar => [ qw(myconfig mailer form) ]
+ scalar => [ qw(myconfig mailer form status extended_status) ]
);
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.
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 {
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};
}
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;
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');
}
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};
}
--- /dev/null
+-- @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();