RT# 73421 Fixed E-Mail pipeline to obey contact opt-in flags
[freeside.git] / httemplate / misc / email-customers.html
1 <%doc>
2
3 Allows emailing one or more customers, based on a search for customers.
4 Search can be specified either through cust_main fields as cgi params, or
5 through a base64 encoded frozen hash in the 'search' cgi param.  Form allows
6 selecting an existing msg_template, or creating a custom message, and shows a
7 preview of the message before sending.  If linked to as a popup, include the
8 cgi parameter 'popup' for proper header handling.
9
10 This may also be used as an element in other pages, enabling you to provide
11 an alternate initial form while using this for search freezing/thawing and 
12 preview/send actions, with the following options:
13
14 acl - the access right to use (defaults to 'Bulk send customer notices')
15
16 form_action - the URL to submit the form to
17
18 process_url - the URL for starting the JSRPC process
19
20 title - the title of the page
21
22 no_search_fields - arrayref of additional fields that are not search parameters
23
24 alternate_form - subroutine that returns alternate html for the initial form,
25 replaces msgnum/from/subject/body/action inputs and submit button, not
26 used if an action is specified
27
28 post_search_hook - sub hook for additional processing after search has been
29 processed from cgi, gets passed options 'conf' and 'search' (a reference to
30 the unfrozen %search hash), should be used to set msgnum or
31 from/subject/body cgi params
32
33 </%doc>
34 % if ($popup) {
35 <& /elements/header-popup.html, $title &>
36 % } else {
37 <& /elements/header.html, $title &>
38 % }
39
40 <& /elements/error.html &>
41
42 <FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
43 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
44 %# Mixing search params with from address, subject, etc. required special-case
45 %# handling of those, risked name conflicts, and caused massive problems with 
46 %# multi-valued search params.  We are no longer in search context, so we 
47 %# pack the search into a Storable string for later use.
48 <INPUT TYPE="hidden" NAME="search" VALUE="<% encode_base64(nfreeze(\%search)) %>">
49 <INPUT TYPE="hidden" NAME="popup" VALUE="<% $popup %>">
50 <INPUT TYPE="hidden" NAME="url" VALUE="<% $url | h %>">
51 <INPUT TYPE="hidden" NAME="to_contact_classnum" VALUE="<% join(',', @contact_classnum) %>">
52
53 % if ( $cgi->param('preview') ) {
54 %   # preview mode: at this point we have a msg_template (either "real" or
55 %   # draft) and $html_body and $text_body contain the preview message.
56 %   # give the user a chance to back out (by going back to edit mode).
57
58     <FONT SIZE="+2">Preview notice</FONT>
59     <& /elements/progress-init.html,
60                  'OneTrueForm',
61                  [ qw( search table msgnum to_contact_classnum emailtovoice_contact custnum ) ],
62                  $process_url,
63                  $pdest,
64     &>
65
66     <TABLE CLASS="fsinnerbox">
67     <INPUT TYPE="hidden" NAME="msgnum" VALUE="<% $msg_template->msgnum %>">
68 %   # kludge these through hidden inputs because they're not really part
69 %   # of the template, but should be sticky during draft editing
70     <INPUT TYPE="hidden" NAME="from_name" VALUE="<% scalar($cgi->param('from_name')) |h %>">
71     <INPUT TYPE="hidden" NAME="from_addr" VALUE="<% scalar($cgi->param('from_addr')) |h %>">
72     <INPUT TYPE="hidden" NAME="emailtovoice_contact" VALUE="<% scalar($cgi->param('emailtovoice_contact')) |h %>">
73     <INPUT TYPE="hidden" NAME="custnum" VALUE="<% scalar($cgi->param('custnum')) |h %>">
74
75 %   if ( !$msg_template->disabled ) {
76       <& /elements/tr-td-label.html, 'label' => 'Template:' &>
77         <td><% $msg_template->msgname |h %></td>
78       </tr>
79 %   }
80
81       <& /elements/tr-td-label.html, 'label' => 'From:' &>
82         <td><% $from |h %></td>
83       </tr>
84
85       <& /elements/tr-td-label.html, 'label' => 'To contacts:' &>
86         <td><% join('<BR>', @contact_classname) %></td>
87       </tr>
88
89       <& /elements/tr-td-label.html, 'label' => 'Subject:' &>
90         <td><% $subject |h %></td>
91       </tr>
92
93       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
94       <TR>
95         <TH ALIGN="right" VALIGN="top">Message (HTML display): </TD>
96         <TD CLASS="background" ALIGN="left"><% $html_body %></TD>
97       </TR>
98
99 %     my $text_body = HTML::FormatText->new(leftmargin=>0)->format(
100 %                       HTML::TreeBuilder->new_from_content(
101 %                         $html_body
102 %                       )
103 %                     );
104       <TR><TD COLSPAN=2>&nbsp;</TD></TR>
105       <TR>
106         <TH ALIGN="right" VALIGN="top">Message (Text display): </TD>
107         <TD CLASS="background" ALIGN="left">
108           <a href="javascript:void(0)" ID="email-message-text-view" style="color:#666666" onclick="showtext()">(view)</a>
109           <a href="javascript:void(0)" ID="email-message-text-hide" style="color:#666666; display: none;" onclick="hidetext()">(hide)</a>
110           <PRE id="email-message-text" style="display: none;"><% $text_body %></PRE>
111         </TD>
112       </TR>
113
114     </TABLE>
115
116     <SCRIPT>
117
118       function showtext() {
119         $('#email-message-text-view').css('display','none');
120         $('#email-message-text-hide').css('display','');
121         $('#email-message-text').slideDown();
122       }
123
124       function hidetext() {
125         $('#email-message-text-view').css('display','');
126         $('#email-message-text-hide').css('display','none');
127         $('#email-message-text').slideUp();
128       }
129
130       function areyousure(href) {
131         if (confirm("Send this notice to <% ($num_cust > 1) ? "$num_cust customers" : '1 customer' %> ?")) {
132           process();
133         }
134       }
135     </SCRIPT>
136
137     <BR>
138     <INPUT TYPE="submit" NAME="edit" VALUE="Edit">
139     <INPUT TYPE="button" VALUE="Send notice" onClick="areyousure()">
140
141 % } elsif ($opt{'alternate_form'}) {
142
143 <% &{$opt{'alternate_form'}}() %>
144
145 % } else {
146 %   # Edit mode.
147
148 <SCRIPT TYPE="text/javascript">
149 function toggle(obj) {
150   document.getElementById('table_no_template').style.display = (obj.value == 0) ? '' : 'none';
151 }
152
153 </SCRIPT>
154 % if ( $msg_template and $msg_template->disabled ) {
155 %   # if we've already established a draft template, don't let msgnum be changed
156     <& /elements/hidden.html,
157       field => 'msgnum',
158       curr_value => ( scalar($cgi->param('msgnum')) || ''),
159     &>
160 % } else {
161 Template: 
162     <& /elements/select-msg_template.html,
163         onchange   => 'toggle(this)',
164         curr_value => ( scalar($cgi->param('msgnum')) || ''),
165     &>
166     <BR>
167 % }
168 % # select destination contact classes
169 <TABLE CELLSPACING=0 id="send_to_contacts_table">
170 <TR>
171  <TD>Send to contacts:</TD>
172  <TD>
173    <div id="contactclassesdiv">
174      <& /elements/checkboxes.html,
175        'style'               => 'display: inline; vertical-align: top',
176        'disable_links'       => 1,
177        'names_list'          => \@contact_checkboxes,
178        'element_name_prefix' => 'contact_class_',
179        'checked_callback'    => sub {
180          # Called for each checkbox
181          # Return true to default as checked, false as unchecked
182          my($cgi, $name) = @_;
183          $name eq 'message'
184        },
185      &>
186    </div>
187 % if ($send_to_domain) {
188    <div>
189      <INPUT TYPE="checkbox" NAME="emailtovoice"  ID="emailtovoice" VALUE="ON" onclick="toggleDiv(this)">Email to voice
190    </div>
191    <div id="emailtovoicediv" style="display:none";>
192
193       <& /elements/select-cust_phone.html,
194                'cust_num'     => $cgi->param('custnum'),
195                'field_name'   => 'emailtovoice_contact',
196                'format'       => 'xxxxxxxxxx',
197                'phone_types'  => [ 'daytime', 'night', 'fax', 'mobile' ],
198       &>@<% $send_to_domain |h %>
199    </div>
200 % }
201  </TD>
202 </TR>
203 </TABLE>
204 <BR>
205 % # if sending a one-off message, show a form to edit it
206   <TABLE BGCOLOR="#cccccc" CELLSPACING=0 WIDTH="100%" id="table_no_template">
207     <& /elements/tr-td-label.html, 'label' => 'From:' &>
208       <TD><& /elements/input-text.html,
209               'field' => 'from_name',
210               'value' => $conf->config('invoice_from_name', $agent_virt_agentnum) ||
211                          $conf->config('company_name', $agent_virt_agentnum), #?
212               'size'  => 20,
213               'curr_value' => scalar($cgi->param('from_name')),
214           &>&nbsp;&lt;\
215           <& /elements/input-text.html,
216               'field' => 'from_addr',
217               'type'  => 'email', # HTML5, woot
218               'value' => $conf->config('invoice_from', $agent_virt_agentnum),
219               'size'  => 20,
220               'curr_value' => scalar($cgi->param('from_addr')),
221           &>&gt;</TD>
222  
223     <& /elements/tr-input-text.html,
224                  'field' => 'subject',
225                  'label' => 'Subject:',
226                  'size'  => 50,
227                  'curr_value' => $subject,
228     &>
229
230     <TR>
231       <TD ALIGN="right" VALIGN="top" STYLE="padding-top:3px">Message: </TD>
232       <TD><& /elements/htmlarea.html, 
233                'field' => 'body',
234                'width' => 763,
235                'curr_value' => $body,
236           &>
237       </TD>
238     </TR>
239
240   </TABLE>
241
242   <INPUT TYPE="hidden" NAME="custnum" VALUE="<% scalar($cgi->param('custnum')) |h %>">
243   <INPUT TYPE="submit" NAME="preview" VALUE="Preview notice">
244
245 % } #end not action or alternate form
246
247 </FORM>
248
249 <SCRIPT TYPE="text/javascript">
250 function toggleDiv(obj) {
251   var box_contactclasses = document.getElementById('contactclassesdiv');
252   var box_emailtovoice = document.getElementById('emailtovoicediv');
253
254   box_emailtovoice.style.display = (box_emailtovoice.style.display == 'none') ? 'block' : 'none';
255   document.getElementById('emailtovoice_contact').options[0].selected=true;
256
257   box_contactclasses.style.display = (box_contactclasses.style.display == 'none') ? 'block' : 'none';
258 }
259 </SCRIPT>
260
261 <& /elements/footer.html &>
262
263 <%init>
264
265 my %opt = @_;
266
267 $opt{'acl'} ||= 'Bulk send customer notices';
268
269 my $email_to;
270
271 die "access denied"
272   unless $FS::CurrentUser::CurrentUser->access_right($opt{'acl'});
273
274 my $conf = FS::Conf->new;
275 my @no_search_fields = qw( table from subject html_body text_body popup url );
276
277 my $send_to_domain = $conf->config('email-to-voice_domain');
278
279 my $form_action = $opt{'form_action'} || 'email-customers.html';
280 my $process_url = $opt{'process_url'} || 'process/email-customers.html';
281 my $title = $opt{'title'} || 'Send customer notices';
282 push( @no_search_fields, @{$opt{'no_search_fields'}} ) if $opt{'no_search_fields'};
283
284 my $table = $cgi->param('table') or die "'table' required";
285 my $agent_virt_agentnum = $cgi->param('agent_virt_agentnum') || '';
286
287 my $popup = $cgi->param('popup');
288 my $url   = $cgi->param('url');
289 my $pdest = { 'message' => "Notice sent" };
290 $pdest->{'url'} = $cgi->param('url') if $url;
291
292 my %search;
293 if ( $cgi->param('search') ) {
294   %search = %{ thaw(decode_base64( $cgi->param('search') )) };
295 }
296 else {
297   %search = $cgi->Vars;
298   delete $search{$_} for @no_search_fields;
299   # FS::$table->search is expected to know which parameters might be 
300   # multi-valued, and to accept scalar values for them also.  No good 
301   # solution to this since CGI can't tell whether a parameter _might_
302   # have had multiple values, only whether it does.
303   @search{keys %search} = map { /\0/ ? [ split /\0/, $_ ] : $_ } values %search;
304 }
305
306 &{$opt{'post_search_hook'}}(
307   'conf'   => $conf,
308   'search' => \%search,
309 ) if $opt{'post_search_hook'};
310
311 my $num_cust;
312 my $from = '';
313 if ( $cgi->param('from') ) {
314   $from = $cgi->param('from');
315 } elsif ( $cgi->param('from_name') ) {
316   $from = ($cgi->param('from_name') . ' <' . $cgi->param('from_addr') . '>');
317 } elsif ( $cgi->param('from_addr') ) {
318   $from = $cgi->param('from_addr');
319 }
320
321 my $msg_template = '';
322 if ( $cgi->param('msgnum') =~ /^(\d+)$/ ) {
323   $msg_template = FS::msg_template->by_key($1)
324     or die "template not found: ".$cgi->param('msgnum');
325 }
326
327 my @contact_classnum;
328 my @contact_classname;
329
330 my $subject = $cgi->param('subject');
331 my $body = $cgi->param('body');
332 my ($html_body, $text_body);
333
334 if ( !$cgi->param('preview') ) {
335
336   # edit mode: initialize the fields from the saved draft, if there is one
337   if ( $msg_template and $msg_template->disabled eq 'D' ) {
338     my $content = $msg_template->content(''); # no localization on these yet
339     $subject ||= $content->subject;
340     $body ||= $content->body;
341   }
342
343 } else {
344
345   my $sql_query = "FS::$table"->search(\%search);
346   my $count_query = delete($sql_query->{'count_query'});
347   my $count_sth = dbh->prepare($count_query)
348     or die "Error preparing $count_query: ". dbh->errstr;
349   $count_sth->execute
350     or die "Error executing $count_query: ". $count_sth->errstr;
351   my $count_arrayref = $count_sth->fetchrow_arrayref;
352   $num_cust = $count_arrayref->[0];
353
354   if ( !$msg_template or $msg_template->disabled eq 'D' ) {
355     # then this is a one-off template; edit it in place
356     my $subject = $cgi->param('subject') || '';
357     my $body = $cgi->param('body') || '';
358
359     # create a draft template
360     $msg_template ||= FS::msg_template->new({
361       msgclass  => 'email',
362       disabled  => 'D',
363     });
364     # anyone have a better idea for msgname?
365     $msg_template->set('msgname' => "Notice " . DateTime->now->iso8601);
366     $msg_template->set('from_addr' => $from);
367     my %content = (
368       subject => $subject,
369       body    => $body,
370     );
371     my $error;
372     if ( $msg_template->msgnum ) {
373       $error = $msg_template->replace(%content);
374     } else {
375       $error = $msg_template->insert(%content);
376     }
377
378     if ( $error ) {
379       $cgi->param('error', $error);
380       $cgi->delete('preview'); # don't go on to preview stage yet
381       undef $msg_template;
382     }
383   }
384   # unless creating the msg_template failed, we now have one, so construct a
385   # preview message from the first customer/whatever in the search results
386
387   my $cust;
388
389   if ( $msg_template ) { 
390     $sql_query->{'extra_sql'} .= ' LIMIT 1';
391     $sql_query->{'select'} = "$table.*";
392     $sql_query->{'order_by'} = '';
393     my $object = qsearchs($sql_query);
394     $cust = $object->cust_main;
395     my %msgopts = (
396       'cust_main' => $cust,
397       'object' => $object,
398     );
399
400     my $cust_msg = $msg_template->prepare(%msgopts);
401     $from = $cust_msg->env_from;
402     $html_body = $cust_msg->preview;
403 #hmm.  this came in with the #37098 rewrite, but isn't on v3 :/
404 # causing problems with mangling subject of unrelated things
405 # should probably decode instead of ignore the UTF-8 thing, but
406 # this at least masks the ugliness for now :/
407     if ( $cust_msg->header =~ /^subject: (.*)/mi && $1 !~ /^\=\?UTF-8/ ) {
408       $subject = $1;
409     }
410   }
411
412   # contact_class_X params
413   #we can't switch to multi_param until we're done supporting deb 7
414   local($CGI::LIST_CONTEXT_WARN) = 0;
415
416   if ($cgi->param('emailtovoice_contact')) {
417     $email_to = $cgi->param('emailtovoice_contact') . '@' . $send_to_domain;
418     push @contact_classnum, 'emailtovoice';
419     push @contact_classname, $email_to;
420   }
421   else {
422     foreach my $param ( $cgi->param ) {
423       if ( $param =~ /^contact_class_(\w+)$/ ) {
424         push @contact_classnum, $1;
425         if ( $1 eq 'invoice' ) {
426           push @contact_classname, 'Invoice recipients';
427         } elsif ( $1 eq 'message' ) {
428           push @contact_classname, 'Message recipients';
429         } else {
430           my $contact_class = FS::contact_class->by_key($1);
431           push @contact_classname, encode_entities($contact_class->classname);
432         }
433       }
434     }
435   }
436 }
437
438 my @contact_checkboxes = (
439   [ 'message' => { label => 'Message recipients' } ],
440   [ 'invoice' => { label => 'Invoice recipients' } ],
441 );
442
443 foreach my $class (qsearch('contact_class', { disabled => '' })) {
444   push @contact_checkboxes, [
445     $class->classnum,
446     { label => $class->classname }
447   ];
448 }
449
450 </%init>