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