<%doc> Allows emailing one or more customers, based on a search for customers. Search can be specified either through cust_main fields as cgi params, or through a base64 encoded frozen hash in the 'search' cgi param. Form allows selecting an existing msg_template, or creating a custom message, and shows a preview of the message before sending. If linked to as a popup, include the cgi parameter 'popup' for proper header handling. This may also be used as an element in other pages, enabling you to provide an alternate initial form while using this for search freezing/thawing and preview/send actions, with the following options: acl - the access right to use (defaults to 'Bulk send customer notices') form_action - the URL to submit the form to process_url - the URL for starting the JSRPC process title - the title of the page no_search_fields - arrayref of additional fields that are not search parameters alternate_form - subroutine that returns alternate html for the initial form, replaces msgnum/from/subject/body/action inputs and submit button, not used if an action is specified post_search_hook - sub hook for additional processing after search has been processed from cgi, gets passed options 'conf' and 'search' (a reference to the unfrozen %search hash), should be used to set msgnum or from/subject/body cgi params % if ($popup) { <& /elements/header-popup.html, $title &> % } else { <& /elements/header.html, $title &> % } <& /elements/error.html &>
%# Mixing search params with from address, subject, etc. required special-case %# handling of those, risked name conflicts, and caused massive problems with %# multi-valued search params. We are no longer in search context, so we %# pack the search into a Storable string for later use. % if ( $cgi->param('preview') ) { % # preview mode: at this point we have a msg_template (either "real" or % # draft) and $html_body and $text_body contain the preview message. % # give the user a chance to back out (by going back to edit mode). Preview notice <& /elements/progress-init.html, 'OneTrueForm', [ qw( search table msgnum to_contact_classnum emailtovoice_contact custnum ) ], $process_url, $pdest, &> % # kludge these through hidden inputs because they're not really part % # of the template, but should be sticky during draft editing % if ( !$msg_template->disabled ) { <& /elements/tr-td-label.html, 'label' => 'Template:' &> % } <& /elements/tr-td-label.html, 'label' => 'From:' &> <& /elements/tr-td-label.html, 'label' => 'To contacts:' &> <& /elements/tr-td-label.html, 'label' => 'Subject:' &> % my $text_body = HTML::FormatText->new(leftmargin=>0)->format( % HTML::TreeBuilder->new_from_content( % $html_body % ) % );
<% $msg_template->msgname |h %>
<% $from |h %>
<% join('
', @contact_classname) %>
<% $subject |h %>
 
Message (HTML display): <% $html_body %>
 
Message (Text display): (view)

% } elsif ($opt{'alternate_form'}) { <% &{$opt{'alternate_form'}}() %> % } else { % # Edit mode. % if ( $msg_template and $msg_template->disabled ) { % # if we've already established a draft template, don't let msgnum be changed <& /elements/hidden.html, field => 'msgnum', curr_value => ( scalar($cgi->param('msgnum')) || ''), &> % } else { Template: <& /elements/select-msg_template.html, onchange => 'toggle(this)', curr_value => ( scalar($cgi->param('msgnum')) || ''), &>
% } % # select destination contact classes % if (@active_classes) { % }
Send to contacts:
<& /elements/checkboxes.html, 'style' => 'display: inline; vertical-align: top', 'disable_links' => 1, 'names_list' => \@optin_checkboxes, 'element_name_prefix' => 'contact_class_', 'checked_callback' => sub { # Called for each checkbox # Return true to default as checked, false as unchecked my($cgi, $name) = @_; exists $dest_ischecked{$name}; }, &>
% if ($send_to_domain) {
Email to voice
% }
Contact Type:
<& /elements/checkboxes.html, 'style' => 'display: inline; vertical-align: top', 'disable_links' => 1, 'names_list' => \@classnum_checkboxes, 'element_name_prefix' => 'contact_class_', 'checked_callback' => sub { # Called for each checkbox # Return true to default as checked, false as unchecked my($cgi, $name) = @_; exists $classnum_ischecked{$name}; }, &>

% # if sending a one-off message, show a form to edit it <& /elements/tr-td-label.html, 'label' => 'From:' &> <& /elements/tr-input-text.html, 'field' => 'subject', 'label' => 'Subject:', 'size' => 50, 'curr_value' => $subject, &>
<& /elements/input-text.html, 'field' => 'from_name', 'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) || $conf->config('company_name', $agent_virt_agentnum), #? 'size' => 20, 'curr_value' => scalar($cgi->param('from_name')), &> <\ <& /elements/input-text.html, 'field' => 'from_addr', 'type' => 'email', # HTML5, woot 'value' => $conf->config('invoice_from', $agent_virt_agentnum), 'size' => 20, 'curr_value' => scalar($cgi->param('from_addr')), &>>
Message: <& /elements/htmlarea.html, 'field' => 'body', 'width' => 763, 'curr_value' => $body, &>
% } #end not action or alternate form
<& /elements/footer.html &> <%init> my %opt = @_; $opt{'acl'} ||= 'Bulk send customer notices'; my $email_to; die "access denied" unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'}); my $conf = FS::Conf->new; my @no_search_fields = qw( table from subject html_body text_body popup url ); my $send_to_domain = $conf->config('email-to-voice_domain'); my $form_action = $opt{'form_action'} || 'email-customers.html'; my $process_url = $opt{'process_url'} || 'process/email-customers.html'; my $title = $opt{'title'} || 'Send customer notices'; push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'}; $m->comp('/elements/handle_uri_query'); my $table = $cgi->param('table') or die "'table' required"; my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || ''; my $popup = $cgi->param('popup'); my $url = $cgi->param('url'); my $pdest = { 'message' => "Notice sent" }; $pdest->{'url'} = $cgi->param('url') if $url; my %search; if ( $cgi->param('search') ) { %search = %{ thaw(decode_base64( $cgi->param('search') )) }; } else { %search = $cgi->Vars; delete $search{$_} for @no_search_fields; # FS::$table->search is expected to know which parameters might be # multi-valued, and to accept scalar values for them also. No good # solution to this since CGI can't tell whether a parameter _might_ # have had multiple values, only whether it does. @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search; } &{$opt{'post_search_hook'}}( 'conf' => $conf, 'search' => \%search, ) if $opt{'post_search_hook'}; my $num_cust; my $from = ''; if ( $cgi->param('from') ) { $from = $cgi->param('from'); } elsif ( $cgi->param('from_name') ) { $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>'); } elsif ( $cgi->param('from_addr') ) { $from = $cgi->param('from_addr'); } my $msg_template = ''; if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) { $msg_template = FS::msg_template->by_key($1) or die "template not found: ".$cgi->param('msgnum'); } my @contact_classnum; my @contact_classname; my $subject = $cgi->param('subject'); my $body = $cgi->param('body'); my ($html_body, $text_body); if ( !$cgi->param('preview') ) { # edit mode: initialize the fields from the saved draft, if there is one if ( $msg_template and $msg_template->disabled eq 'D' ) { my $content = $msg_template->content(''); # no localization on these yet $subject ||= $content->subject; $body ||= $content->body; } } else { my @checked_email_dest; my @checked_contact_type; for ($cgi->param) { if (/^contact_class_(.+)$/) { my $f = $1; if ($f eq 'invoice' || $f eq 'message') { push @checked_email_dest, $f; } elsif ( $f =~ /^\d+$/ ) { push @checked_contact_type, $f; } } } $search{with_email_dest} = \@checked_email_dest if @checked_email_dest; $search{with_contact_type} = \@checked_contact_type if @checked_contact_type; my $sql_query = "FS::$table"->search(\%search); my $count_query = delete($sql_query->{'count_query'}); my $count_sth = dbh->prepare($count_query) or die "Error preparing $count_query: ". dbh->errstr; $count_sth->execute or die "Error executing $count_query: ". $count_sth->errstr; my $count_arrayref = $count_sth->fetchrow_arrayref; $num_cust = $count_arrayref->[0]; if ( !$msg_template or $msg_template->disabled eq 'D' ) { # then this is a one-off template; edit it in place my $subject = $cgi->param('subject') || ''; my $body = $cgi->param('body') || ''; # create a draft template $msg_template ||= FS::msg_template->new({ msgclass => 'email', disabled => 'D', }); # anyone have a better idea for msgname? $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601); $msg_template->set('from_addr' => $from); my %content = ( subject => $subject, body => $body, ); my $error; if ( $msg_template->msgnum ) { $error = $msg_template->replace(%content); } else { $error = $msg_template->insert(%content); } if ( $error ) { $cgi->param('error', $error); $cgi->delete('preview'); # don't go on to preview stage yet undef $msg_template; } } # unless creating the msg_template failed, we now have one, so construct a # preview message from the first customer/whatever in the search results my $cust; if ( $msg_template ) { $sql_query->{'extra_sql'} .= ' LIMIT 1'; $sql_query->{'select'} = "$table.*"; $sql_query->{'order_by'} = ''; my $object = qsearchs($sql_query); # Could use better error handling here... die "No customers match the search criteria" unless ref $object; $cust = $object->cust_main; my %msgopts = ( 'cust_main' => $cust, 'object' => $object, ); my $cust_msg = $msg_template->prepare(%msgopts); $from = $cust_msg->env_from; $html_body = $cust_msg->preview; #hmm. this came in with the #37098 rewrite, but isn't on v3 :/ # causing problems with mangling subject of unrelated things # should probably decode instead of ignore the UTF-8 thing, but # this at least masks the ugliness for now :/ if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) { $subject = $1; } } # contact_class_X params #we can't switch to multi_param until we're done supporting deb 7 local($CGI::LIST_CONTEXT_WARN) = 0; if ($cgi->param('emailtovoice_contact')) { $email_to = $cgi->param('emailtovoice_contact') . '@' . $send_to_domain; push @contact_classnum, 'emailtovoice'; push @contact_classname, $email_to; } else { foreach my $param ( $cgi->param ) { if ( $param =~ /^contact_class_(\w+)$/ ) { push @contact_classnum, $1; if ( $1 eq 'invoice' ) { push @contact_classname, 'Invoice recipients'; } elsif ( $1 eq 'message' ) { push @contact_classname, 'Message recipients'; } else { my $contact_class = FS::contact_class->by_key($1); push @contact_classname, encode_entities( $contact_class ? $contact_class->classname : '(none)' ); } } } } } # Build data structures for "Opt In" and "Contact Type" checkboxes # # By default, message recipients will be selected, this is a message. # By default, all Contact Types will be selected, but this may be # overridden by passing 'classnums' get/post values. If no contact # types have been defined, the option will not be presented. my @active_classes = qsearch(contact_class => {disabled => ''} ); my %classnum_ischecked; my %dest_ischecked; $CGI::LIST_CONTEXT_WARN = 0; if ( my @in_classnums = $cgi->param('classnums') ) { # Set checked boxes from form input for my $v (@in_classnums) { if ( $v =~ /^\d+$/ ) { $classnum_ischecked{$v} = 1 } elsif ( $v =~ /^(invoice|message)$/ ) { $dest_ischecked{$v} = 1; } } } else { # Checked boxes default values $classnum_ischecked{$_->classnum} = 1 for @active_classes; $classnum_ischecked{0} = 1; } # At least one destination is required $dest_ischecked{message} = 1 unless %dest_ischecked; my @optin_checkboxes = ( [ 'message' => { label => 'Message recipients' } ], [ 'invoice' => { label => 'Invoice recipients' } ], ); my @classnum_checkboxes = ( [ '0' => { label => '(None)' }], map { [ $_->classnum => {label => $_->classname} ] } @active_classes, );