From bb119c4cc86e906f698a205437790bd8f96bb3d0 Mon Sep 17 00:00:00 2001 From: mark Date: Fri, 20 May 2011 20:19:44 +0000 Subject: [PATCH] logging of template-generated mail, #12809 --- FS/FS/Conf.pm | 7 ++ FS/FS/Mason.pm | 1 + FS/FS/Misc.pm | 36 +++++++- FS/FS/Schema.pm | 18 ++++ FS/FS/cust_msg.pm | 154 +++++++++++++++++++++++++++++++++++ FS/FS/msg_template.pm | 30 +++++-- FS/MANIFEST | 2 + FS/t/cust_msg.t | 5 ++ httemplate/elements/menu.html | 2 + httemplate/misc/email-customers.html | 2 +- httemplate/search/cust_msg.html | 122 +++++++++++++++++++++++++++ httemplate/view/cust_msg.html | 55 +++++++++++++ 12 files changed, 424 insertions(+), 10 deletions(-) create mode 100644 FS/FS/cust_msg.pm create mode 100644 FS/t/cust_msg.t create mode 100644 httemplate/search/cust_msg.html create mode 100755 httemplate/view/cust_msg.html diff --git a/FS/FS/Conf.pm b/FS/FS/Conf.pm index d5828f0c0..d83ac2f98 100644 --- a/FS/FS/Conf.pm +++ b/FS/FS/Conf.pm @@ -623,6 +623,13 @@ my %payment_gateway_options = ( }, { + 'key' => 'log_sent_mail', + 'section' => 'notification', + 'description' => 'Enable logging of template-generated email.', + 'type' => 'checkbox', + }, + + { 'key' => 'alert_expiration', 'section' => 'notification', 'description' => 'Enable alerts about billing method expiration (i.e. expiring credit cards).', diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm index f97db5421..e8e66e844 100644 --- a/FS/FS/Mason.pm +++ b/FS/FS/Mason.pm @@ -285,6 +285,7 @@ if ( -e $addl_handler_use_file ) { use FS::did_order_item; use FS::msa; use FS::rate_center; + use FS::cust_msg; # Sammath Naur if ( $FS::Mason::addl_handler_use ) { diff --git a/FS/FS/Misc.pm b/FS/FS/Misc.pm index a55f4a912..d5f02de1d 100644 --- a/FS/FS/Misc.pm +++ b/FS/FS/Misc.pm @@ -89,6 +89,11 @@ encoding which, if specified, overrides the default "7bit". (optional) type parameter for multipart/related messages +=item cust_msg + +(optional) L object. If provided, it will be updated +with the message envelope information, contents, and server response. + =back =cut @@ -171,12 +176,13 @@ sub send_email { } my $message_id = join('.', rand()*(2**32), $$, time). "\@$domain"; + my $time = time; my $message = MIME::Entity->build( 'From' => $options{'from'}, 'To' => join(', ', @to), 'Sender' => $options{'from'}, 'Reply-To' => $options{'from'}, - 'Date' => time2str("%a, %d %b %Y %X %z", time), + 'Date' => time2str("%a, %d %b %Y %X %z", $time), 'Subject' => $options{'subject'}, 'Message-ID' => "<$message_id>", @mimeargs, @@ -238,13 +244,30 @@ sub send_email { eval { sendmail($message, { transport => $transport, from => $options{from}, to => \@to }) }; - + + my $error = ''; if(ref($@) and $@->isa('Email::Sender::Failure')) { - return ($@->code ? $@->code.' ' : '').$@->message + $error = $@->code.' ' if $@->code; + $error .= $@->message; } else { - return $@; + $error = $@; } + + # Logging + my $cust_msg = $options{'cust_msg'}; + if ( $cust_msg ) { + $cust_msg->env_from($options{from}); + $cust_msg->env_to(join(",", @to)); + $cust_msg->header($message->header_as_string); + $cust_msg->body($message->body_as_string); + $cust_msg->_date($time); + $cust_msg->error($error); + $cust_msg->status( $error ? 'failed' : 'sent' ); + $cust_msg->replace; + }; + return $error; + } =item generate_email OPTION => VALUE ... @@ -279,6 +302,10 @@ Will be placed inside an HTML tag. Email body (Text alternative). Arrayref of lines, or scalar. +=item cust_msg (optional) + +An L object. Will be passed through to send_email. + =back Constructs a multipart message from text_body and html_body. @@ -300,6 +327,7 @@ sub generate_email { 'to' => $args{'to'}, 'bcc' => $args{'bcc'}, 'subject' => $args{'subject'}, + 'cust_msg'=> $args{'cust_msg'}, ); #if (ref($args{'to'}) eq 'ARRAY') { diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index eeab4d1a2..819fae9f5 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -3354,6 +3354,24 @@ sub tables_hashref { 'index' => [ ['agentnum'], ] }, + 'cust_msg' => { + 'columns' => [ + 'custmsgnum', 'serial', '', '', '', '', + 'custnum', 'int', '', '', '', '', + 'msgnum', 'int', 'NULL', '', '', '', + '_date', @date_type, '', '', + 'env_from', 'varchar', 'NULL', 255, '', '', + 'env_to', 'varchar', 'NULL', 255, '', '', + 'header', 'blob', 'NULL', '', '', '', + 'body', 'blob', 'NULL', '', '', '', + 'error', 'varchar', 'NULL', 255, '', '', + 'status', 'varchar', '',$char_d, '', '', + ], + 'primary_key' => 'custmsgnum', + 'unique' => [ ], + 'index' => [ ['custnum'], ], + }, + 'svc_cert' => { 'columns' => [ 'svcnum', 'int', '', '', '', '', diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm new file mode 100644 index 000000000..c9cf68663 --- /dev/null +++ b/FS/FS/cust_msg.pm @@ -0,0 +1,154 @@ +package FS::cust_msg; + +use strict; +use base qw( FS::cust_main_Mixin FS::Record ); +use FS::Record qw( qsearch qsearchs ); +use vars qw( @statuses ); + +=head1 NAME + +FS::cust_msg - Object methods for cust_msg records + +=head1 SYNOPSIS + + use FS::cust_msg; + + $record = new FS::cust_msg \%hash; + $record = new FS::cust_msg { 'column' => 'value' }; + + $error = $record->insert; + + $error = $record->check; + +=head1 DESCRIPTION + +An FS::cust_msg object represents a template-generated message sent to +a customer (see L). FS::cust_msg inherits from +FS::Record. The following fields are currently supported: + +=over 4 + +=item custmsgnum - primary key + +=item custnum - customer number + +=item msgnum - template number + +=item _date - the time the message was sent + +=item env_from - envelope From address + +=item env_to - envelope To addresses, including Bcc, separated by newlines + +=item header - message header + +=item body - message body + +=item error - Email::Sender error message (or null for success) + +=back + +=head1 METHODS + +=over 4 + +=item new HASHREF + +Creates a new + +=cut + +# the new method can be inherited from FS::Record, if a table method is defined + +sub table { 'cust_msg'; } + +sub nohistory_fields { ('header', 'body'); } +# history is kind of pointless on this table + +@statuses = qw( prepared sent failed ); + +=item insert + +Adds this record to the database. If there is an error, returns the error +and emits a warning; otherwise returns false. + +=cut + +sub insert { + # warn of all errors here; failing to insert/update one of these should + # cause a warning at worst + my $self = shift; + my $error = $self->SUPER::insert; + warn "[cust_msg] error logging message status: $error\n" if $error; + return $error; +} + +=item delete + +Delete this record from the database. There's no reason to do this. + +=cut + +sub delete { + my $self = shift; + warn "[cust_msg] log entry deleted\n"; + return $self->SUPER::delete; +} + +=item replace OLD_RECORD + +Replaces the OLD_RECORD with this one in the database. If there is an error, +returns the error and emits a warning, otherwise returns false. + +=cut + +sub replace { + my $self = shift; + my $error = $self->SUPER::replace(@_); + warn "[cust_msg] error logging message status: $error\n" if $error; + return $error; +} + +=item check + +Checks all fields to make sure this is a valid example. If there is +an error, returns the error, otherwise returns false. Called by the insert +and replace methods. + +=cut + +# the check method should currently be supplied - FS::Record contains some +# data checking routines + +sub check { + my $self = shift; + + my $error = + $self->ut_numbern('custmsgnum') + || $self->ut_number('custnum') + || $self->ut_foreign_key('custnum', 'cust_main', 'custnum') + || $self->ut_numbern('msgnum') + || $self->ut_foreign_keyn('msgnum', 'msg_template', 'msgnum') + || $self->ut_numbern('_date') + || $self->ut_textn('env_from') + || $self->ut_textn('env_to') + || $self->ut_anything('header') + || $self->ut_anything('body') + || $self->ut_enum('status', \@statuses) + || $self->ut_textn('error') + ; + return $error if $error; + + $self->SUPER::check; +} + +=back + +=head1 SEE ALSO + +L, L, L. + +=cut + +1; + diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 73284d1e5..4a1e34584 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -7,11 +7,16 @@ use FS::Misc qw( generate_email send_email ); use FS::Conf; use FS::Record qw( qsearch qsearchs ); +use FS::cust_main; +use FS::cust_msg; + use Date::Format qw( time2str ); use HTML::Entities qw( decode_entities encode_entities ) ; use HTML::FormatText; use HTML::TreeBuilder; -use vars '$DEBUG'; +use vars qw( $DEBUG $conf ); + +FS::UID->install_callback( sub { $conf = new FS::Conf; } ); $DEBUG=0; @@ -188,6 +193,11 @@ The I field in the template takes precedence over this. Destination address. The default is to use the customer's invoicing_list addresses. Multiple addresses may be comma-separated. +=item preview + +Set to true when preparing a message for previewing, rather than to actually +send it. This turns off logging. + =back =cut @@ -298,7 +308,6 @@ sub prepare { } # no warning when preparing with no destination - my $conf = new FS::Conf; my $from_addr = $self->from_addr; if ( !$from_addr ) { @@ -309,8 +318,20 @@ sub prepare { $from_addr ||= scalar( $conf->config('invoice_from', $cust_main->agentnum) ); } + my @cust_msg = (); + if ( $conf->exists('log_sent_mail') and !$opt{'preview'} ) { + my $cust_msg = FS::cust_msg->new({ + 'custnum' => $cust_main->custnum, + 'msgnum' => $self->msgnum, + 'status' => 'prepared', + }); + $cust_msg->insert; + @cust_msg = ('cust_msg' => $cust_msg); + } ( + 'custnum' => $cust_main->custnum, + 'msgnum' => $self->msgnum, 'from' => $from_addr, 'to' => \@to, 'bcc' => $self->bcc_addr || undef, @@ -318,6 +339,7 @@ sub prepare { 'html_body' => $body, 'text_body' => HTML::FormatText->new(leftmargin => 0, rightmargin => 70 )->format( HTML::TreeBuilder->new_from_content($body) ), + @cust_msg, ); } @@ -339,8 +361,7 @@ sub send { # helper sub for package dates my $ymd = sub { $_[0] ? time2str('%Y-%m-%d', $_[0]) : '' }; -# needed for some things -my $conf = new FS::Conf; +#my $conf = new FS::Conf; #return contexts and fill-in values # If you add anything, be sure to add a description in @@ -504,7 +525,6 @@ sub _upgrade_data { [ 'warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', '' ], ); - my $conf = new FS::Conf; my @agentnums = ('', map {$_->agentnum} qsearch('agent', {})); foreach my $agentnum (@agentnums) { foreach (@fixes) { diff --git a/FS/MANIFEST b/FS/MANIFEST index 3b80da8fc..5e6d992ba 100644 --- a/FS/MANIFEST +++ b/FS/MANIFEST @@ -594,6 +594,8 @@ FS/msa.pm t/msa.t FS/rate_center.pm t/rate_center.t +FS/cust_msg.pm +t/cust_msg.t FS/Locales.pm t/Locales.t FS/L10N.pm diff --git a/FS/t/cust_msg.t b/FS/t/cust_msg.t new file mode 100644 index 000000000..5d6e439e9 --- /dev/null +++ b/FS/t/cust_msg.t @@ -0,0 +1,5 @@ +BEGIN { $| = 1; print "1..1\n" } +END {print "not ok 1\n" unless $loaded;} +use FS::cust_msg; +$loaded=1; +print "ok 1\n"; diff --git a/httemplate/elements/menu.html b/httemplate/elements/menu.html index 437392237..d0632a971 100644 --- a/httemplate/elements/menu.html +++ b/httemplate/elements/menu.html @@ -414,6 +414,8 @@ $tools_menu{'Time Queue'} = [ $fsurl.'search/report_timeworked.html', 'View pen if $curuser->access_right('Time queue'); $tools_menu{'Attachments'} = [ $fsurl.'browse/cust_attachment.html', 'View customer attachments' ] if !$conf->config('disable_cust_attachment') and $curuser->access_right('View attachments') and $curuser->access_right('Browse attachments'); +$tools_menu{'Outgoing messages'} = [ $fsurl.'search/cust_msg.html', 'View outgoing message log' ] + if $curuser->access_right('View customers of all agents'); $tools_menu{'Importing'} = [ \%tools_importing, 'Import tools' ] if $curuser->access_right('Import'); $tools_menu{'Exporting'} = [ \%tools_exporting, 'Export tools' ] diff --git a/httemplate/misc/email-customers.html b/httemplate/misc/email-customers.html index 759c8bf94..97ad8d8d4 100644 --- a/httemplate/misc/email-customers.html +++ b/httemplate/misc/email-customers.html @@ -186,7 +186,7 @@ if ( $cgi->param('action') eq 'preview' ) { $sql_query->{'extra_sql'} .= ' LIMIT 1'; $sql_query->{'order_by'} = ''; my $cust = qsearchs($sql_query)->cust_main; - my %message = $msg_template->prepare( 'cust_main' => $cust ); + my %message = $msg_template->prepare( 'cust_main' => $cust, 'preview' => 1 ); ($from, $subject, $html_body) = @message{'from', 'subject', 'html_body'}; } } diff --git a/httemplate/search/cust_msg.html b/httemplate/search/cust_msg.html new file mode 100644 index 000000000..7932ab3a2 --- /dev/null +++ b/httemplate/search/cust_msg.html @@ -0,0 +1,122 @@ +<& 'elements/search.html', + 'title' => $title, + 'name' => 'messages', + 'query' => $query, + 'count_query' => $count_query, + 'header' => [ + 'Date', + 'Template', + 'Destination', + 'Status', + '', #error + ], + 'fields' => [ + sub { + my $date = $_[0]->_date; + $date ? time2str('%Y-%m-%d %T',$_[0]->_date) : '' + }, + 'msgname', + 'env_to', + 'status', + sub { encode_entities($_[0]->error) }, + ], + 'align' => 'rllcl', + 'links' => [ ], + 'link_onclicks' => [ + $sub_popup_link, + $sub_popup_link, + $sub_popup_link, + '', + '', + ], + 'color' => [ ('') x 3, + $statuscolor, + $statuscolor, + ], + 'html_init' => $html_init, + 'really_disable_download' => 1, +&> +<%init> +#hmm... +die "access denied" + unless $FS::CurrentUser::CurrentUser->access_right('View customers of all agents'); + +my $conf = new FS::Conf; + +my $title = 'Outgoing Message Log'; + +my @where; +if ( $cgi->param('status') =~ /^(\w+)$/ ) { + push @where, "status = '$1'"; +} +my ($beginning, $ending) = FS::UI::Web::parse_beginning_ending($cgi, ''); +push @where, "(_date >= $beginning AND _date <= $ending)"; + +my $order_by = '_date'; +if ( $cgi->param('order_by') =~ /^(\w+)$/ ) { + $order_by = $1; +} + +my $where = ''; +$where = ' WHERE '.join(' AND ', @where) if @where; +my $query = { + 'table' => 'cust_msg', + 'select' => join(', ', + 'cust_msg.*', + 'msg_template.msgname', + ), + 'addl_from' => ' LEFT JOIN msg_template USING ( msgnum )', + 'hashref' => {}, + 'extra_sql' => $where, + 'order_by' => "ORDER BY $order_by", +}; +my $count_query = 'SELECT COUNT(*) FROM cust_msg'.$where; + +my $sub_popup_link = sub { + my $custmsgnum = $_[0]->custmsgnum; + include('/elements/popup_link_onclick.html', + 'action' => $p. 'view/cust_msg.html?' . $custmsgnum, + 'actionlabel' => 'Message detail', + 'width' => 600, + 'height' => 500, + ); +}; + +my %color = ( + 'prepared' => '0000FF', + 'failed' => 'FF0000', + 'sent' => '', +); +my $statuscolor = sub { $color{$_[0]->status} }; + +my $html_init = qq!
+!. +' + +
From '. +include('/elements/input-date-field.html', + { 'name' => 'beginning', 'value' => $cgi->param('beginning') } +). +' To '. +include('/elements/input-date-field.html', + { 'name' => 'ending', 'value' => ($cgi->param('ending') || ''), + 'noinit' => 1, } +). +' Status '. +include('/elements/select.html', + 'field' => 'status', + 'curr_value' => $cgi->param('status') || '', + 'options' => [ '', 'failed', 'sent', 'prepared' ], + 'labels' => { '' => '(any)', + 'failed' => 'failed', + 'sent' => 'sent', + 'prepared'=> 'prepared' }, +) . +'

+'; + + diff --git a/httemplate/view/cust_msg.html b/httemplate/view/cust_msg.html new file mode 100755 index 000000000..a5846109a --- /dev/null +++ b/httemplate/view/cust_msg.html @@ -0,0 +1,55 @@ +<& /elements/header-popup.html &> + + + +% if ( $date ) { + +% } +% if ( $cust_msg->error ) { + +% } + + +
From:<% $cust_msg->env_from %>
To:<% $env_to %>
<% $label{$cust_msg->status} %><% $date %>
Error:<% encode_entities($cust_msg->error) %>
+
+ + Header + Body +
+
+ + +
+ +<& /elements/footer.html &> +<%init> + +my $curuser = $FS::CurrentUser::CurrentUser; +die "access denied" if !$curuser->access_right('View customers of all agents'); + +my ($custmsgnum) = $cgi->keywords; +$custmsgnum =~ /^(\d+)$/ or die "illegal custmsgnum"; +my $cust_msg = qsearchs('cust_msg', { 'custmsgnum' => $custmsgnum }); +my $date = ''; +$date = time2str('%Y-%m-%d %T', $cust_msg->_date) if ( $cust_msg->_date ); +my $env_to = join('', split(',', $cust_msg->env_to)); + +my %label = ( + 'sent' => 'Sent:', + 'failed' => 'Attempted: ', + 'prepared' => 'Not sent', +); + -- 2.11.0