summaryrefslogtreecommitdiff
path: root/FS/FS
diff options
context:
space:
mode:
authorMark Wells <mark@freeside.biz>2015-08-27 19:18:42 -0700
committerMark Wells <mark@freeside.biz>2015-08-30 17:48:49 -0700
commit5a2a2f8a6f95738758b43cdbbfa48dd7830de2d5 (patch)
tree98500c31f92e741212d0d7c3355e4f744e0102f1 /FS/FS
parent11c81c66b62ac176c167583f7b68ed80bd4239c9 (diff)
#21564, external message services: preview and send messages through the UI
Diffstat (limited to 'FS/FS')
-rw-r--r--FS/FS/Schema.pm1
-rw-r--r--FS/FS/cust_main_Mixin.pm41
-rw-r--r--FS/FS/cust_msg.pm10
-rw-r--r--FS/FS/msg_template.pm2
-rw-r--r--FS/FS/msg_template/email.pm448
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