diff options
author | Mark Wells <mark@freeside.biz> | 2015-08-27 19:18:42 -0700 |
---|---|---|
committer | Mark Wells <mark@freeside.biz> | 2015-08-30 17:48:49 -0700 |
commit | 5a2a2f8a6f95738758b43cdbbfa48dd7830de2d5 (patch) | |
tree | 98500c31f92e741212d0d7c3355e4f744e0102f1 /FS/FS | |
parent | 11c81c66b62ac176c167583f7b68ed80bd4239c9 (diff) |
#21564, external message services: preview and send messages through the UI
Diffstat (limited to 'FS/FS')
-rw-r--r-- | FS/FS/Schema.pm | 1 | ||||
-rw-r--r-- | FS/FS/cust_main_Mixin.pm | 41 | ||||
-rw-r--r-- | FS/FS/cust_msg.pm | 10 | ||||
-rw-r--r-- | FS/FS/msg_template.pm | 2 | ||||
-rw-r--r-- | FS/FS/msg_template/email.pm | 448 |
5 files changed, 88 insertions, 414 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 311313a..12211d1 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -6400,6 +6400,7 @@ sub tables_hashref { 'error', 'varchar', 'NULL', 255, '', '', 'status', 'varchar', '',$char_d, '', '', 'msgtype', 'varchar', 'NULL', 16, '', '', + 'preview', 'text', 'NULL', '', '', '', ], 'primary_key' => 'custmsgnum', 'unique' => [ ], diff --git a/FS/FS/cust_main_Mixin.pm b/FS/FS/cust_main_Mixin.pm index bdad511..3d05f84 100644 --- a/FS/FS/cust_main_Mixin.pm +++ b/FS/FS/cust_main_Mixin.pm @@ -445,6 +445,10 @@ sub email_search_result { my $success = 0; my %sent_to = (); + if ( !$msg_template ) { + # XXX create on the fly + } + #eventually order+limit magic to reduce memory use? foreach my $obj ( qsearch($sql_query) ) { @@ -459,36 +463,19 @@ sub email_search_result { } my $cust_main = $obj->cust_main; - tie my %message, 'Tie::IxHash'; if ( !$cust_main ) { next; # unlinked object; nothing else we can do } - if ( $msg_template ) { - # Now supports other context objects. - %message = $msg_template->prepare( - 'cust_main' => $cust_main, - 'object' => $obj, - ); - } - else { - my @to = $cust_main->invoicing_list_emailonly; - next if !@to; - - %message = ( - 'from' => $from, - 'to' => \@to, - 'subject' => $subject, - 'html_body' => $html_body, - 'text_body' => $text_body, - 'custnum' => $cust_main->custnum, - ); - } #if $msg_template + my $cust_msg = $msg_template->prepare( + 'cust_main' => $cust_main, + 'object' => $obj, + ); # For non-cust_main searches, we avoid duplicates based on message - # body text. + # body text. my $unique = $cust_main->custnum; - $unique .= sha1($message{'text_body'}) if $class ne 'FS::cust_main'; + $unique .= sha1($cust_msg->text_body) if $class ne 'FS::cust_main'; if( $sent_to{$unique} ) { # avoid duplicates $dups++; @@ -497,18 +484,20 @@ sub email_search_result { $sent_to{$unique} = 1; - $error = send_email( generate_email( %message ) ); + $error = $cust_msg->send; if($error) { # queue the sending of this message so that the user can see what we # tried to do, and retry if desired + # (note the cust_msg itself also now has a status of 'failed'; that's + # fine, as it will get its status reset if we retry the job) my $queue = new FS::queue { - 'job' => 'FS::Misc::process_send_email', + 'job' => 'FS::cust_msg::process_send', 'custnum' => $cust_main->custnum, 'status' => 'failed', 'statustext' => $error, }; - $queue->insert(%message); + $queue->insert($cust_msg->custmsgnum); push @retry_jobs, $queue; } else { diff --git a/FS/FS/cust_msg.pm b/FS/FS/cust_msg.pm index 9346327..ec2c961 100644 --- a/FS/FS/cust_msg.pm +++ b/FS/FS/cust_msg.pm @@ -47,8 +47,12 @@ from FS::Record. The following fields are currently supported: =item body - message body (as a complete MIME document) +=item preview - HTML fragment to show as a preview of the message + =item error - Email::Sender error message (or null for success) +=item status - "prepared", "sent", or "failed" + =back =head1 METHODS @@ -137,6 +141,7 @@ sub check { || $self->ut_textn('env_to') || $self->ut_anything('header') || $self->ut_anything('body') + || $self->ut_anything('preview') || $self->ut_enum('status', \@statuses) || $self->ut_textn('error') || $self->ut_enum('msgtype', [ '', @@ -159,8 +164,9 @@ message on error, or an empty string. sub send { my $self = shift; - my $msg_template = $self->msg_template - or return 'message was created without a template object'; + # it's still allowed to have cust_msgs without message templates, but only + # for email. + my $msg_template = $self->msg_template || 'FS::msg_template::email'; $msg_template->send_prepared($self); } diff --git a/FS/FS/msg_template.pm b/FS/FS/msg_template.pm index 180e9de..d7d9f50 100644 --- a/FS/FS/msg_template.pm +++ b/FS/FS/msg_template.pm @@ -10,6 +10,8 @@ use FS::Record qw( qsearch qsearchs ); use FS::cust_msg; use FS::template_content; +use Date::Format qw(time2str); + FS::UID->install_callback( sub { $conf = new FS::Conf; } ); $DEBUG=0; diff --git a/FS/FS/msg_template/email.pm b/FS/FS/msg_template/email.pm index 1133faa..275dc82 100644 --- a/FS/FS/msg_template/email.pm +++ b/FS/FS/msg_template/email.pm @@ -26,11 +26,12 @@ use FS::Record qw( qsearch qsearchs ); use FS::template_content; use FS::UID qw( dbh ); +# needed to manage prepared messages use FS::cust_msg; FS::UID->install_callback( sub { $conf = new FS::Conf; } ); -our $DEBUG = 1; +our $DEBUG = 0; our $me = '[FS::msg_template::email]'; =head1 NAME @@ -362,74 +363,12 @@ sub prepare { 'error' => '', 'status' => 'prepared', 'msgtype' => ($opt{'msgtype'} || ''), + 'preview' => $body, # html content only }); return $cust_msg; } -=item send_prepared CUST_MSG - -Takes the CUST_MSG object and sends it to its recipient. - -=cut - -sub send_prepared { - my $self = shift; - my $cust_msg = shift or die "cust_msg required"; - - my $domain = 'example.com'; - if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) { - $domain = $1; - } - - my @to = split(/\s*,\s*/, $cust_msg->env_to); - - my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), - 'helo' => $domain ); - - my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); - $smtp_opt{'port'} = $port; - - my $transport; - if ( defined($enc) && $enc eq 'starttls' ) { - $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); - $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); - } else { - if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { - $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); - } - $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; - $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); - } - - warn "$me sending message\n" if $DEBUG; - my $message = join("\n\n", $cust_msg->header, $cust_msg->body); - local $@; - eval { - sendmail( $message, { transport => $transport, - from => $cust_msg->env_from, - to => \@to }) - }; - my $error = ''; - if(ref($@) and $@->isa('Email::Sender::Failure')) { - $error = $@->code.' ' if $@->code; - $error .= $@->message; - } - else { - $error = $@; - } - - $cust_msg->set('error', $error); - $cust_msg->set('status', $error ? 'failed' : 'sent'); - if ( $cust_msg->custmsgnum ) { - $cust_msg->replace; - } else { - $cust_msg->insert; - } - - $error; -} - =item render OPTION => VALUE ... Fills in the template and renders it to a PDF document. Returns the @@ -491,183 +430,6 @@ my $usage_warning = sub { return ['', '', '']; }; -#my $conf = new FS::Conf; - -#return contexts and fill-in values -# If you add anything, be sure to add a description in -# httemplate/edit/msg_template.html. -sub substitutions { - { 'cust_main' => [qw( - display_custnum agentnum agent_name - - last first company - name name_short contact contact_firstlast - address1 address2 city county state zip - country - daytime night mobile fax - - has_ship_address - ship_name ship_name_short ship_contact ship_contact_firstlast - ship_address1 ship_address2 ship_city ship_county ship_state ship_zip - ship_country - - paymask payname paytype payip - num_cancelled_pkgs num_ncancelled_pkgs num_pkgs - classname categoryname - balance - credit_limit - invoicing_list_emailonly - cust_status ucfirst_cust_status cust_statuscolor cust_status_label - - signupdate dundate - packages recurdates - ), - [ invoicing_email => sub { shift->invoicing_list_emailonly_scalar } ], - #compatibility: obsolete ship_ fields - use the non-ship versions - map ( - { my $field = $_; - [ "ship_$field" => sub { shift->$field } ] - } - qw( last first company daytime night fax ) - ), - # ship_name, ship_name_short, ship_contact, ship_contact_firstlast - # still work, though - [ expdate => sub { shift->paydate_epoch } ], #compatibility - [ signupdate_ymd => sub { $ymd->(shift->signupdate) } ], - [ dundate_ymd => sub { $ymd->(shift->dundate) } ], - [ paydate_my => sub { sprintf('%02d/%04d', shift->paydate_monthyear) } ], - [ otaker_first => sub { shift->access_user->first } ], - [ otaker_last => sub { shift->access_user->last } ], - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ company_name => sub { - $conf->config('company_name', shift->agentnum) - } ], - [ company_address => sub { - $conf->config('company_address', shift->agentnum) - } ], - [ company_phonenum => sub { - $conf->config('company_phonenum', shift->agentnum) - } ], - [ selfservice_server_base_url => sub { - $conf->config('selfservice_server-base_url') #, shift->agentnum) - } ], - ], - # next_bill_date - 'cust_pkg' => [qw( - pkgnum pkg_label pkg_label_long - location_label - status statuscolor - - start_date setup bill last_bill - adjourn susp expire - labels_short - ), - [ pkg => sub { shift->part_pkg->pkg } ], - [ pkg_category => sub { shift->part_pkg->categoryname } ], - [ pkg_class => sub { shift->part_pkg->classname } ], - [ cancel => sub { shift->getfield('cancel') } ], # grrr... - [ start_ymd => sub { $ymd->(shift->getfield('start_date')) } ], - [ setup_ymd => sub { $ymd->(shift->getfield('setup')) } ], - [ next_bill_ymd => sub { $ymd->(shift->getfield('bill')) } ], - [ last_bill_ymd => sub { $ymd->(shift->getfield('last_bill')) } ], - [ adjourn_ymd => sub { $ymd->(shift->getfield('adjourn')) } ], - [ susp_ymd => sub { $ymd->(shift->getfield('susp')) } ], - [ expire_ymd => sub { $ymd->(shift->getfield('expire')) } ], - [ cancel_ymd => sub { $ymd->(shift->getfield('cancel')) } ], - - # not necessarily correct for non-flat packages - [ setup_fee => sub { shift->part_pkg->option('setup_fee') } ], - [ recur_fee => sub { shift->part_pkg->option('recur_fee') } ], - - [ freq_pretty => sub { shift->part_pkg->freq_pretty } ], - - ], - 'cust_bill' => [qw( - invnum - _date - _date_pretty - due_date - ), - [ due_date2str => sub { shift->due_date2str('short') } ], - ], - #XXX not really thinking about cust_bill substitutions quite yet - - # for welcome and limit warning messages - 'svc_acct' => [qw( - svcnum - username - domain - ), - [ password => sub { shift->getfield('_password') } ], - [ column => sub { &$usage_warning(shift)->[0] } ], - [ amount => sub { &$usage_warning(shift)->[1] } ], - [ threshold => sub { &$usage_warning(shift)->[2] } ], - ], - 'svc_domain' => [qw( - svcnum - domain - ), - [ registrar => sub { - my $registrar = qsearchs('registrar', - { registrarnum => shift->registrarnum} ); - $registrar ? $registrar->registrarname : '' - } - ], - [ catchall => sub { - my $svc_acct = qsearchs('svc_acct', { svcnum => shift->catchall }); - $svc_acct ? $svc_acct->email : '' - } - ], - ], - 'svc_phone' => [qw( - svcnum - phonenum - countrycode - domain - ) - ], - 'svc_broadband' => [qw( - svcnum - speed_up - speed_down - ip_addr - mac_addr - ) - ], - # for payment receipts - 'cust_pay' => [qw( - paynum - _date - ), - [ paid => sub { sprintf("%.2f", shift->paid) } ], - # overrides the one in cust_main in cases where a cust_pay is passed - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $cust_pay = shift; - ($cust_pay->payby eq 'CARD' || $cust_pay->payby eq 'CHEK') ? - $cust_pay->paymask : $cust_pay->decrypt($cust_pay->payinfo) - } ], - ], - # for payment decline messages - # try to support all cust_pay fields - # 'error' is a special case, it contains the raw error from the gateway - 'cust_pay_pending' => [qw( - _date - error - ), - [ paid => sub { sprintf("%.2f", shift->paid) } ], - [ payby => sub { FS::payby->shortname(shift->payby) } ], - [ date => sub { time2str("%a %B %o, %Y", shift->_date) } ], - [ payinfo => sub { - my $pending = shift; - ($pending->payby eq 'CARD' || $pending->payby eq 'CHEK') ? - $pending->paymask : $pending->decrypt($pending->payinfo) - } ], - ], - }; -} - =item content LOCALE Returns the L<FS::template_content> object appropriate to LOCALE, if there @@ -684,168 +446,84 @@ sub content { { 'msgnum' => $self->msgnum, 'locale' => '' }); } -=item agent - -Returns the L<FS::agent> object for this template. - =cut -sub _upgrade_data { - my ($self, %opts) = @_; +=back - ### - # First move any historical templates in config to real message templates - ### +=head2 CLASS METHODS - my @fixes = ( - [ 'alerter_msgnum', 'alerter_template', '', '', '' ], - [ 'cancel_msgnum', 'cancelmessage', 'cancelsubject', '', '' ], - [ 'decline_msgnum', 'declinetemplate', '', '', '' ], - [ 'impending_recur_msgnum', 'impending_recur_template', '', '', 'impending_recur_bcc' ], - [ 'payment_receipt_msgnum', 'payment_receipt_email', '', '', '' ], - [ 'welcome_msgnum', 'welcome_email', 'welcome_email-subject', 'welcome_email-from', '' ], - [ 'warning_msgnum', 'warning_email', 'warning_email-subject', 'warning_email-from', '' ], - ); - - my @agentnums = ('', map {$_->agentnum} qsearch('agent', {})); - foreach my $agentnum (@agentnums) { - foreach (@fixes) { - my ($newname, $oldname, $subject, $from, $bcc) = @$_; - if ($conf->exists($oldname, $agentnum)) { - my $new = new FS::msg_template({ - 'msgname' => $oldname, - 'agentnum' => $agentnum, - 'from_addr' => ($from && $conf->config($from, $agentnum)) || '', - 'bcc_addr' => ($bcc && $conf->config($from, $agentnum)) || '', - 'subject' => ($subject && $conf->config($subject, $agentnum)) || '', - 'mime_type' => 'text/html', - 'body' => join('<BR>',$conf->config($oldname, $agentnum)), - }); - my $error = $new->insert; - die $error if $error; - $conf->set($newname, $new->msgnum, $agentnum); - $conf->delete($oldname, $agentnum); - $conf->delete($from, $agentnum) if $from; - $conf->delete($subject, $agentnum) if $subject; - } - } +=over 4 - if ( $conf->exists('alert_expiration', $agentnum) ) { - my $msgnum = $conf->exists('alerter_msgnum', $agentnum); - my $template = FS::msg_template->by_key($msgnum) if $msgnum; - if (!$template) { - warn "template for alerter_msgnum $msgnum not found\n"; - next; - } - # this is now a set of billing events - foreach my $days (30, 15, 5) { - my $event = FS::part_event->new({ - 'agentnum' => $agentnum, - 'event' => "Card expiration warning - $days days", - 'eventtable' => 'cust_main', - 'check_freq' => '1d', - 'action' => 'notice', - 'disabled' => 'Y', #initialize first - }); - my $error = $event->insert( 'msgnum' => $msgnum ); - if ($error) { - warn "error creating expiration alert event:\n$error\n\n"; - next; - } - # make it work like before: - # only send each warning once before the card expires, - # only warn active customers, - # only warn customers with CARD/DCRD, - # only warn customers who get email invoices - my %conds = ( - 'once_every' => { 'run_delay' => '30d' }, - 'cust_paydate_within' => { 'within' => $days.'d' }, - 'cust_status' => { 'status' => { 'active' => 1 } }, - 'payby' => { 'payby' => { 'CARD' => 1, - 'DCRD' => 1, } - }, - 'message_email' => {}, - ); - foreach (keys %conds) { - my $condition = FS::part_event_condition->new({ - 'conditionname' => $_, - 'eventpart' => $event->eventpart, - }); - $error = $condition->insert( %{ $conds{$_} }); - if ( $error ) { - warn "error creating expiration alert event:\n$error\n\n"; - next; - } - } - $error = $event->initialize; - if ( $error ) { - warn "expiration alert event was created, but not initialized:\n$error\n\n"; - } - } # foreach $days - $conf->delete('alerter_msgnum', $agentnum); - $conf->delete('alert_expiration', $agentnum); - - } # if alerter_msgnum +=item send_prepared CUST_MSG - } +Takes the CUST_MSG object and sends it to its recipient. This is a class +method because everything needed to send the message is stored in the +CUST_MSG already. - ### - # Move subject and body from msg_template to template_content - ### +=cut - foreach my $msg_template ( qsearch('msg_template', {}) ) { - if ( $msg_template->subject || $msg_template->body ) { - # create new default content - my %content; - $content{subject} = $msg_template->subject; - $msg_template->set('subject', ''); - - # work around obscure Pg/DBD bug - # https://rt.cpan.org/Public/Bug/Display.html?id=60200 - # (though the right fix is to upgrade DBD) - my $body = $msg_template->body; - if ( $body =~ /^x([0-9a-f]+)$/ ) { - # there should be no real message templates that look like that - warn "converting template body to TEXT\n"; - $body = pack('H*', $1); - } - $content{body} = $body; - $msg_template->set('body', ''); +sub send_prepared { + my $self = shift; + my $cust_msg = shift or die "cust_msg required"; - my $error = $msg_template->replace(%content); - die $error if $error; - } + my $domain = 'example.com'; + if ( $cust_msg->env_from =~ /\@([\w\.\-]+)/ ) { + $domain = $1; } - ### - # Add new-style default templates if missing - ### - $self->_populate_initial_data; - -} + my @to = split(/\s*,\s*/, $cust_msg->env_to); -sub _populate_initial_data { #class method - #my($class, %opts) = @_; - #my $class = shift; + my %smtp_opt = ( 'host' => $conf->config('smtpmachine'), + 'helo' => $domain ); - eval "use FS::msg_template::InitialData;"; - die $@ if $@; + my($port, $enc) = split('-', ($conf->config('smtp-encryption') || '25') ); + $smtp_opt{'port'} = $port; + + my $transport; + if ( defined($enc) && $enc eq 'starttls' ) { + $smtp_opt{$_} = $conf->config("smtp-$_") for qw(username password); + $transport = Email::Sender::Transport::SMTP::TLS->new( %smtp_opt ); + } else { + if ( $conf->exists('smtp-username') && $conf->exists('smtp-password') ) { + $smtp_opt{"sasl_$_"} = $conf->config("smtp-$_") for qw(username password); + } + $smtp_opt{'ssl'} = 1 if defined($enc) && $enc eq 'tls'; + $transport = Email::Sender::Transport::SMTP->new( %smtp_opt ); + } - my $initial_data = FS::msg_template::InitialData->_initial_data; + warn "$me sending message\n" if $DEBUG; + my $message = join("\n\n", $cust_msg->header, $cust_msg->body); + local $@; + eval { + sendmail( $message, { transport => $transport, + from => $cust_msg->env_from, + to => \@to }) + }; + my $error = ''; + if(ref($@) and $@->isa('Email::Sender::Failure')) { + $error = $@->code.' ' if $@->code; + $error .= $@->message; + } + else { + $error = $@; + } - foreach my $hash ( @$initial_data ) { + $cust_msg->set('error', $error); + $cust_msg->set('status', $error ? 'failed' : 'sent'); + if ( $cust_msg->custmsgnum ) { + $cust_msg->replace; + } else { + $cust_msg->insert; + } - next if $hash->{_conf} && $conf->config( $hash->{_conf} ); + $error; +} - my $msg_template = new FS::msg_template($hash); - my $error = $msg_template->insert( @{ $hash->{_insert_args} || [] } ); - die $error if $error; +=back - $conf->set( $hash->{_conf}, $msg_template->msgnum ) if $hash->{_conf}; - - } +=cut -} +# internal use only sub eviscerate { # Every bit as pleasant as it sounds. @@ -897,8 +575,6 @@ sub eviscerate { (\@outside, \@inside); } -=back - =head1 BUGS =head1 SEE ALSO |