Merge pull request #7797 from JKingsnorth/CRM-17977
[civicrm-core.git] / CRM / Contact / Form / Task / EmailCommon.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.7 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2016 |
7 +--------------------------------------------------------------------+
8 | This file is a part of CiviCRM. |
9 | |
10 | CiviCRM is free software; you can copy, modify, and distribute it |
11 | under the terms of the GNU Affero General Public License |
12 | Version 3, 19 November 2007 and the CiviCRM Licensing Exception. |
13 | |
14 | CiviCRM is distributed in the hope that it will be useful, but |
15 | WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. |
17 | See the GNU Affero General Public License for more details. |
18 | |
19 | You should have received a copy of the GNU Affero General Public |
20 | License and the CiviCRM Licensing Exception along |
21 | with this program; if not, contact CiviCRM LLC |
22 | at info[AT]civicrm[DOT]org. If you have questions about the |
23 | GNU Affero General Public License or the licensing of CiviCRM, |
24 | see the CiviCRM license FAQ at http://civicrm.org/licensing |
25 +--------------------------------------------------------------------+
26 */
27
28 /**
29 *
30 * @package CRM
31 * @copyright CiviCRM LLC (c) 2004-2016
32 */
33
34 /**
35 * This class provides the common functionality for sending email to
36 * one or a group of contact ids. This class is reused by all the search
37 * components in CiviCRM (since they all have send email as a task)
38 */
39 class CRM_Contact_Form_Task_EmailCommon {
40 const MAX_EMAILS_KILL_SWITCH = 50;
41
42 public $_contactDetails = array();
43 public $_allContactDetails = array();
44 public $_toContactEmails = array();
45
46 /**
47 * @param CRM_Core_Form $form
48 */
49 public static function preProcessFromAddress(&$form) {
50 $form->_single = FALSE;
51 $className = CRM_Utils_System::getClassName($form);
52 if (property_exists($form, '_context') &&
53 $form->_context != 'search' &&
54 $className == 'CRM_Contact_Form_Task_Email'
55 ) {
56 $form->_single = TRUE;
57 }
58
59 $form->_emails = $emails = array();
60
61 $session = CRM_Core_Session::singleton();
62 $contactID = $session->get('userID');
63
64 $form->_contactIds = array($contactID);
65 $contactEmails = CRM_Core_BAO_Email::allEmails($contactID);
66
67 $form->_onHold = array();
68
69 $fromDisplayName = CRM_Core_DAO::getFieldValue('CRM_Contact_DAO_Contact',
70 $contactID, 'display_name'
71 );
72
73 foreach ($contactEmails as $emailId => $item) {
74 $email = $item['email'];
75 if (!$email && (count($emails) < 1)) {
76 // set it if no emails are present at all
77 $form->_noEmails = TRUE;
78 }
79 else {
80 if ($email) {
81 if (in_array($email, $emails)) {
82 // CRM-3624
83 continue;
84 }
85
86 $emails[$emailId] = '"' . $fromDisplayName . '" <' . $email . '> ';
87 $form->_onHold[$emailId] = $item['on_hold'];
88 $form->_noEmails = FALSE;
89 }
90 }
91
92 $form->_emails[$emailId] = $emails[$emailId];
93 $emails[$emailId] .= $item['locationType'];
94
95 if ($item['is_primary']) {
96 $emails[$emailId] .= ' ' . ts('(preferred)');
97 }
98 $emails[$emailId] = htmlspecialchars($emails[$emailId]);
99 }
100
101 $form->assign('noEmails', $form->_noEmails);
102
103 if ($form->_noEmails) {
104 CRM_Core_Error::statusBounce(ts('Your user record does not have a valid email address'));
105 }
106
107 // now add domain from addresses
108 $domainEmails = array();
109 $domainFrom = CRM_Core_OptionGroup::values('from_email_address');
110 foreach (array_keys($domainFrom) as $k) {
111 $domainEmail = $domainFrom[$k];
112 $domainEmails[$domainEmail] = htmlspecialchars($domainEmail);
113 $form->_emails[$domainEmail] = $domainEmail;
114 }
115
116 $form->_fromEmails = CRM_Utils_Array::crmArrayMerge($emails, $domainEmails);
117
118 // Add signature
119 $defaultEmail = civicrm_api3('email', 'getsingle', array('id' => key($form->_fromEmails)));
120 $defaults = array();
121 if (!empty($defaultEmail['signature_html'])) {
122 $defaults['html_message'] = '<br/><br/>--' . $defaultEmail['signature_html'];
123 }
124 if (!empty($defaultEmail['signature_text'])) {
125 $defaults['text_message'] = "\n\n--\n" . $defaultEmail['signature_text'];
126 }
127 $form->setDefaults($defaults);
128 }
129
130 /**
131 * Build the form object.
132 *
133 * @param CRM_Core_Form $form
134 */
135 public static function buildQuickForm(&$form) {
136 $toArray = $ccArray = $bccArray = array();
137 $suppressedEmails = 0;
138 //here we are getting logged in user id as array but we need target contact id. CRM-5988
139 $cid = $form->get('cid');
140 if ($cid) {
141 $form->_contactIds = explode(',', $cid);
142 }
143 if (count($form->_contactIds) > 1) {
144 $form->_single = FALSE;
145 }
146
147 $emailAttributes = array(
148 'class' => 'huge',
149 );
150 $to = $form->add('text', 'to', ts('To'), $emailAttributes, TRUE);
151 $cc = $form->add('text', 'cc_id', ts('CC'), $emailAttributes);
152 $bcc = $form->add('text', 'bcc_id', ts('BCC'), $emailAttributes);
153
154 $setDefaults = TRUE;
155 if (property_exists($form, '_context') && $form->_context == 'standalone') {
156 $setDefaults = FALSE;
157 }
158
159 $elements = array('to', 'cc', 'bcc');
160 $form->_allContactIds = $form->_toContactIds = $form->_contactIds;
161 foreach ($elements as $element) {
162 if ($$element->getValue()) {
163 $allEmails = explode(',', $$element->getValue());
164 if ($element == 'to') {
165 $form->_toContactIds = $form->_contactIds = array();
166 }
167
168 foreach ($allEmails as $value) {
169 list($contactId, $email) = explode('::', $value);
170 if ($contactId) {
171 switch ($element) {
172 case 'to':
173 $form->_contactIds[] = $form->_toContactIds[] = $contactId;
174 $form->_toContactEmails[] = $email;
175 break;
176
177 case 'cc':
178 $form->_ccContactIds[] = $contactId;
179 break;
180
181 case 'bcc':
182 $form->_bccContactIds[] = $contactId;
183 break;
184 }
185
186 $form->_allContactIds[] = $contactId;
187 }
188 }
189
190 $setDefaults = TRUE;
191 }
192 }
193
194 //get the group of contacts as per selected by user in case of Find Activities
195 if (!empty($form->_activityHolderIds)) {
196 $contact = $form->get('contacts');
197 $form->_allContactIds = $form->_contactIds = $contact;
198 }
199
200 // check if we need to setdefaults and check for valid contact emails / communication preferences
201 if (is_array($form->_allContactIds) && $setDefaults) {
202 $returnProperties = array(
203 'sort_name' => 1,
204 'email' => 1,
205 'do_not_email' => 1,
206 'is_deceased' => 1,
207 'on_hold' => 1,
208 'display_name' => 1,
209 'preferred_mail_format' => 1,
210 );
211
212 // get the details for all selected contacts ( to, cc and bcc contacts )
213 list($form->_contactDetails) = CRM_Utils_Token::getTokenDetails($form->_allContactIds,
214 $returnProperties,
215 FALSE,
216 FALSE
217 );
218
219 // make a copy of all contact details
220 $form->_allContactDetails = $form->_contactDetails;
221
222 // perform all validations
223 foreach ($form->_allContactIds as $key => $contactId) {
224 $value = $form->_contactDetails[$contactId];
225 if ($value['do_not_email'] || empty($value['email']) || !empty($value['is_deceased']) || $value['on_hold']) {
226 $suppressedEmails++;
227
228 // unset contact details for contacts that we won't be sending email. This is prevent extra computation
229 // during token evaluation etc.
230 unset($form->_contactDetails[$contactId]);
231 }
232 else {
233 $email = $value['email'];
234
235 // build array's which are used to setdefaults
236 if (in_array($contactId, $form->_toContactIds)) {
237 $form->_toContactDetails[$contactId] = $form->_contactDetails[$contactId];
238 // If a particular address has been specified as the default, use that instead of contact's primary email
239 if (!empty($form->_toEmail) && $form->_toEmail['contact_id'] == $contactId) {
240 $email = $form->_toEmail['email'];
241 }
242 $toArray[] = array(
243 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
244 'id' => "$contactId::{$email}",
245 );
246 }
247 elseif (in_array($contactId, $form->_ccContactIds)) {
248 $ccArray[] = array(
249 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
250 'id' => "$contactId::{$email}",
251 );
252 }
253 elseif (in_array($contactId, $form->_bccContactIds)) {
254 $bccArray[] = array(
255 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
256 'id' => "$contactId::{$email}",
257 );
258 }
259 }
260 }
261
262 if (empty($toArray)) {
263 CRM_Core_Error::statusBounce(ts('Selected contact(s) do not have a valid email address, or communication preferences specify DO NOT EMAIL, or they are deceased or Primary email address is On Hold.'));
264 }
265 }
266
267 $form->assign('toContact', json_encode($toArray));
268 $form->assign('ccContact', json_encode($ccArray));
269 $form->assign('bccContact', json_encode($bccArray));
270
271 $form->assign('suppressedEmails', $suppressedEmails);
272
273 $form->assign('totalSelectedContacts', count($form->_contactIds));
274
275 $form->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE);
276
277 $form->add('select', 'fromEmailAddress', ts('From'), $form->_fromEmails, TRUE, array('class' => 'crm-select2 huge'));
278
279 CRM_Mailing_BAO_Mailing::commonCompose($form);
280
281 // add attachments
282 CRM_Core_BAO_File::buildAttachment($form, NULL);
283
284 if ($form->_single) {
285 // also fix the user context stack
286 if ($form->_caseId) {
287 $ccid = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $form->_caseId,
288 'contact_id', 'case_id'
289 );
290 $url = CRM_Utils_System::url('civicrm/contact/view/case',
291 "&reset=1&action=view&cid={$ccid}&id={$form->_caseId}"
292 );
293 }
294 elseif ($form->_context) {
295 $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1');
296 }
297 else {
298 $url = CRM_Utils_System::url('civicrm/contact/view',
299 "&show=1&action=browse&cid={$form->_contactIds[0]}&selectedChild=activity"
300 );
301 }
302
303 $session = CRM_Core_Session::singleton();
304 $session->replaceUserContext($url);
305 $form->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
306 }
307 else {
308 $form->addDefaultButtons(ts('Send Email'), 'upload');
309 }
310
311 $fields = array(
312 'followup_assignee_contact_id' => array(
313 'type' => 'entityRef',
314 'label' => ts('Assigned to'),
315 'attributes' => array(
316 'multiple' => TRUE,
317 'create' => TRUE,
318 'api' => array('params' => array('is_deceased' => 0)),
319 ),
320 ),
321 'followup_activity_type_id' => array(
322 'type' => 'select',
323 'label' => ts('Followup Activity'),
324 'attributes' => array('' => '- ' . ts('select activity') . ' -') + CRM_Core_PseudoConstant::ActivityType(FALSE),
325 'extra' => array('class' => 'crm-select2'),
326 ),
327 'followup_activity_subject' => array(
328 'type' => 'text',
329 'label' => ts('Subject'),
330 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity',
331 'subject'
332 ),
333 ),
334 );
335
336 //add followup date
337 $form->addDateTime('followup_date', ts('in'), FALSE, array('formatType' => 'activityDateTime'));
338
339 foreach ($fields as $field => $values) {
340 if (!empty($fields[$field])) {
341 $attribute = CRM_Utils_Array::value('attributes', $values);
342 $required = !empty($values['required']);
343
344 if ($values['type'] == 'select' && empty($attribute)) {
345 $form->addSelect($field, array('entity' => 'activity'), $required);
346 }
347 elseif ($values['type'] == 'entityRef') {
348 $form->addEntityRef($field, $values['label'], $attribute, $required);
349 }
350 else {
351 $form->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values));
352 }
353 }
354 }
355
356 $form->addFormRule(array('CRM_Contact_Form_Task_EmailCommon', 'formRule'), $form);
357 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
358 }
359
360 /**
361 * Form rule.
362 *
363 * @param array $fields
364 * The input form values.
365 * @param array $dontCare
366 * @param array $self
367 * Additional values form 'this'.
368 *
369 * @return bool|array
370 * true if no errors, else array of errors
371 */
372 public static function formRule($fields, $dontCare, $self) {
373 $errors = array();
374 $template = CRM_Core_Smarty::singleton();
375
376 if (isset($fields['html_message'])) {
377 $htmlMessage = str_replace(array("\n", "\r"), ' ', $fields['html_message']);
378 $htmlMessage = str_replace('"', '\"', $htmlMessage);
379 $template->assign('htmlContent', $htmlMessage);
380 }
381
382 //Added for CRM-1393
383 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
384 $errors['saveTemplateName'] = ts("Enter name to save message template");
385 }
386
387 return empty($errors) ? TRUE : $errors;
388 }
389
390 /**
391 * Process the form after the input has been submitted and validated.
392 *
393 * @param CRM_Core_Form $form
394 */
395 public static function postProcess(&$form) {
396 if (count($form->_contactIds) > self::MAX_EMAILS_KILL_SWITCH) {
397 CRM_Core_Error::fatal(ts('Please do not use this task to send a lot of emails (greater than %1). We recommend using CiviMail instead.',
398 array(1 => self::MAX_EMAILS_KILL_SWITCH)
399 ));
400 }
401
402 // check and ensure that
403 $formValues = $form->controller->exportValues($form->getName());
404 $fromEmail = $formValues['fromEmailAddress'];
405 $from = CRM_Utils_Array::value($fromEmail, $form->_emails);
406 $subject = $formValues['subject'];
407
408 // CRM-13378: Append CC and BCC information at the end of Activity Details and format cc and bcc fields
409 $elements = array('cc_id', 'bcc_id');
410 $additionalDetails = NULL;
411 $ccValues = $bccValues = array();
412 foreach ($elements as $element) {
413 if (!empty($formValues[$element])) {
414 $allEmails = explode(',', $formValues[$element]);
415 foreach ($allEmails as $value) {
416 list($contactId, $email) = explode('::', $value);
417 $contactURL = CRM_Utils_System::url('civicrm/contact/view', "reset=1&force=1&cid={$contactId}", TRUE);
418 switch ($element) {
419 case 'cc_id':
420 $ccValues['email'][] = '"' . $form->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>';
421 $ccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails[$contactId]['display_name'] . "</a>";
422 break;
423
424 case 'bcc_id':
425 $bccValues['email'][] = '"' . $form->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>';
426 $bccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails[$contactId]['display_name'] . "</a>";
427 break;
428 }
429 }
430 }
431 }
432
433 $cc = $bcc = '';
434 if (!empty($ccValues)) {
435 $cc = implode(',', $ccValues['email']);
436 $additionalDetails .= "\ncc : " . implode(", ", $ccValues['details']);
437 }
438 if (!empty($bccValues)) {
439 $bcc = implode(',', $bccValues['email']);
440 $additionalDetails .= "\nbcc : " . implode(", ", $bccValues['details']);
441 }
442
443 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
444 if (isset($form->_caseId) && is_numeric($form->_caseId)) {
445 $hash = substr(sha1(CIVICRM_SITE_KEY . $form->_caseId), 0, 7);
446 $subject = "[case #$hash] $subject";
447 }
448
449 // process message template
450 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
451 $messageTemplate = array(
452 'msg_text' => $formValues['text_message'],
453 'msg_html' => $formValues['html_message'],
454 'msg_subject' => $formValues['subject'],
455 'is_active' => TRUE,
456 );
457
458 if (!empty($formValues['saveTemplate'])) {
459 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
460 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
461 }
462
463 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
464 $messageTemplate['id'] = $formValues['template'];
465 unset($messageTemplate['msg_title']);
466 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
467 }
468 }
469
470 $attachments = array();
471 CRM_Core_BAO_File::formatAttachment($formValues,
472 $attachments,
473 NULL, NULL
474 );
475
476 // format contact details array to handle multiple emails from same contact
477 $formattedContactDetails = array();
478 $tempEmails = array();
479 foreach ($form->_contactIds as $key => $contactId) {
480 // if we dont have details on this contactID, we should ignore
481 // potentially this is due to the contact not wanting to receive email
482 if (!isset($form->_contactDetails[$contactId])) {
483 continue;
484 }
485 $email = $form->_toContactEmails[$key];
486 // prevent duplicate emails if same email address is selected CRM-4067
487 // we should allow same emails for different contacts
488 $emailKey = "{$contactId}::{$email}";
489 if (!in_array($emailKey, $tempEmails)) {
490 $tempEmails[] = $emailKey;
491 $details = $form->_contactDetails[$contactId];
492 $details['email'] = $email;
493 unset($details['email_id']);
494 $formattedContactDetails[] = $details;
495 }
496 }
497
498 // send the mail
499 list($sent, $activityId) = CRM_Activity_BAO_Activity::sendEmail(
500 $formattedContactDetails,
501 $subject,
502 $formValues['text_message'],
503 $formValues['html_message'],
504 NULL,
505 NULL,
506 $from,
507 $attachments,
508 $cc,
509 $bcc,
510 array_keys($form->_toContactDetails),
511 $additionalDetails
512 );
513
514 $followupStatus = '';
515 if ($sent) {
516 $followupActivity = NULL;
517 if (!empty($formValues['followup_activity_type_id'])) {
518 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
519 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
520 $params['followup_date'] = $formValues['followup_date'];
521 $params['followup_date_time'] = $formValues['followup_date_time'];
522 $params['target_contact_id'] = $form->_contactIds;
523 $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']);
524 $followupActivity = CRM_Activity_BAO_Activity::createFollowupActivity($activityId, $params);
525 $followupStatus = ts('A followup activity has been scheduled.');
526
527 if (Civi::settings()->get('activity_assignee_notification')) {
528 if ($followupActivity) {
529 $mailToFollowupContacts = array();
530 $assignee = array($followupActivity->id);
531 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment::getAssigneeNames($assignee, TRUE, FALSE);
532 foreach ($assigneeContacts as $values) {
533 $mailToFollowupContacts[$values['email']] = $values;
534 }
535
536 $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts);
537 if ($sentFollowup) {
538 $followupStatus .= '<br />' . ts("A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).");
539 }
540 }
541 }
542 }
543
544 $count_success = count($form->_toContactDetails);
545 CRM_Core_Session::setStatus(ts('One message was sent successfully. ', array(
546 'plural' => '%count messages were sent successfully. ',
547 'count' => $count_success,
548 )) . $followupStatus, ts('Message Sent', array('plural' => 'Messages Sent', 'count' => $count_success)), 'success');
549 }
550
551 // Display the name and number of contacts for those email is not sent.
552 // php 5.4 throws out a notice since the values of these below arrays are arrays.
553 // the behavior is not documented in the php manual, but it does the right thing
554 // suppressing the notices to get things in good shape going forward
555 $emailsNotSent = @array_diff_assoc($form->_allContactDetails, $form->_contactDetails);
556
557 if ($emailsNotSent) {
558 $not_sent = array();
559 foreach ($emailsNotSent as $contactId => $values) {
560 $displayName = $values['display_name'];
561 $email = $values['email'];
562 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid=$contactId");
563 $not_sent[] = "<a href='$contactViewUrl' title='$email'>$displayName</a>" . ($values['on_hold'] ? '(' . ts('on hold') . ')' : '');
564 }
565 $status = '(' . ts('because no email address on file or communication preferences specify DO NOT EMAIL or Contact is deceased or Primary email address is On Hold') . ')<ul><li>' . implode('</li><li>', $not_sent) . '</li></ul>';
566 CRM_Core_Session::setStatus($status, ts('One Message Not Sent', array(
567 'count' => count($emailsNotSent),
568 'plural' => '%count Messages Not Sent',
569 )), 'info');
570 }
571
572 if (isset($form->_caseId)) {
573 // if case-id is found in the url, create case activity record
574 $cases = explode(',', $form->_caseId);
575 foreach ($cases as $key => $val) {
576 if (is_numeric($val)) {
577 $caseParams = array(
578 'activity_id' => $activityId,
579 'case_id' => $val,
580 );
581 CRM_Case_BAO_Case::processCaseActivity($caseParams);
582 }
583 }
584 }
585 }
586
587 }