3 +--------------------------------------------------------------------+
4 | Copyright CiviCRM LLC. All rights reserved. |
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 +--------------------------------------------------------------------+
15 * @copyright CiviCRM LLC https://civicrm.org/licensing
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)
23 class CRM_Contact_Form_Task_EmailCommon
{
25 const MAX_EMAILS_KILL_SWITCH
= 50;
27 public $_contactDetails = [];
29 public $_allContactDetails = [];
31 public $_toContactEmails = [];
34 * Pre Process Form Addresses to be used in Quickform
36 * @param CRM_Core_Form $form
37 * @param bool $bounce determine if we want to throw a status bounce.
39 * @throws \CiviCRM_API3_Exception
41 public static function preProcessFromAddress(&$form, $bounce = TRUE) {
42 if (!isset($form->_single
)) {
43 // @todo ensure this is already set.
44 $form->_single
= FALSE;
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()];
54 $fromEmailValues = CRM_Core_BAO_Email
::getFromEmail();
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.'));
62 $form->_emails
= $fromEmailValues;
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));
68 if (is_numeric(key($form->_fromEmails
))) {
70 $defaultEmail = civicrm_api3('email', 'getsingle', ['id' => key($form->_fromEmails
)]);
72 if (!empty($defaultEmail['signature_html'])) {
73 $defaults['html_message'] = '<br/><br/>--' . $defaultEmail['signature_html'];
75 if (!empty($defaultEmail['signature_text'])) {
76 $defaults['text_message'] = "\n\n--\n" . $defaultEmail['signature_text'];
79 $form->setDefaults($defaults);
83 * Build the form object.
85 * @param CRM_Core_Form $form
87 * @throws \CRM_Core_Exception
89 public static function buildQuickForm(&$form) {
90 CRM_Core_Error
::deprecatedFunctionWarning('This code is no longer used in core and will be removed');
91 $toArray = $ccArray = $bccArray = [];
92 $suppressedEmails = 0;
93 //here we are getting logged in user id as array but we need target contact id. CRM-5988
94 $cid = $form->get('cid');
96 $form->_contactIds
= explode(',', $cid);
98 if (count($form->_contactIds
) > 1) {
99 $form->_single
= FALSE;
101 CRM_Contact_Form_Task_EmailCommon
::bounceIfSimpleMailLimitExceeded(count($form->_contactIds
));
106 $to = $form->add('text', 'to', ts('To'), $emailAttributes, TRUE);
107 $cc = $form->add('text', 'cc_id', ts('CC'), $emailAttributes);
108 $bcc = $form->add('text', 'bcc_id', ts('BCC'), $emailAttributes);
110 if ($to->getValue()) {
111 $form->_toContactIds
= $form->_contactIds
= [];
114 if (property_exists($form, '_context') && $form->_context
== 'standalone') {
115 $setDefaults = FALSE;
118 $elements = ['to', 'cc', 'bcc'];
119 $form->_allContactIds
= $form->_toContactIds
= $form->_contactIds
;
120 foreach ($elements as $element) {
121 if ($
$element->getValue()) {
123 foreach (self
::getEmails($
$element) as $value) {
124 $contactId = $value['contact_id'];
125 $email = $value['email'];
129 $form->_contactIds
[] = $form->_toContactIds
[] = $contactId;
130 $form->_toContactEmails
[] = $email;
134 $form->_ccContactIds
[] = $contactId;
138 $form->_bccContactIds
[] = $contactId;
142 $form->_allContactIds
[] = $contactId;
150 //get the group of contacts as per selected by user in case of Find Activities
151 if (!empty($form->_activityHolderIds
)) {
152 $contact = $form->get('contacts');
153 $form->_allContactIds
= $form->_contactIds
= $contact;
156 // check if we need to setdefaults and check for valid contact emails / communication preferences
157 if (is_array($form->_allContactIds
) && $setDefaults) {
158 $returnProperties = [
165 'preferred_mail_format' => 1,
168 // get the details for all selected contacts ( to, cc and bcc contacts )
169 list($form->_contactDetails
) = CRM_Utils_Token
::getTokenDetails($form->_allContactIds
,
175 // make a copy of all contact details
176 $form->_allContactDetails
= $form->_contactDetails
;
178 // perform all validations on unique contact Ids
179 foreach (array_unique($form->_allContactIds
) as $key => $contactId) {
180 $value = $form->_contactDetails
[$contactId];
181 if ($value['do_not_email'] ||
empty($value['email']) ||
!empty($value['is_deceased']) ||
$value['on_hold']) {
184 // unset contact details for contacts that we won't be sending email. This is prevent extra computation
185 // during token evaluation etc.
186 unset($form->_contactDetails
[$contactId]);
189 $email = $value['email'];
191 // build array's which are used to setdefaults
192 if (in_array($contactId, $form->_toContactIds
)) {
193 $form->_toContactDetails
[$contactId] = $form->_contactDetails
[$contactId];
194 // If a particular address has been specified as the default, use that instead of contact's primary email
195 if (!empty($form->_toEmail
) && $form->_toEmail
['contact_id'] == $contactId) {
196 $email = $form->_toEmail
['email'];
199 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
200 'id' => "$contactId::{$email}",
203 elseif (in_array($contactId, $form->_ccContactIds
)) {
205 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
206 'id' => "$contactId::{$email}",
209 elseif (in_array($contactId, $form->_bccContactIds
)) {
211 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
212 'id' => "$contactId::{$email}",
218 if (empty($toArray)) {
219 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.'));
223 $form->assign('toContact', json_encode($toArray));
224 $form->assign('ccContact', json_encode($ccArray));
225 $form->assign('bccContact', json_encode($bccArray));
227 $form->assign('suppressedEmails', $suppressedEmails);
229 $form->assign('totalSelectedContacts', count($form->_contactIds
));
231 $form->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE);
233 $form->add('select', 'from_email_address', ts('From'), $form->_fromEmails
, TRUE);
235 CRM_Mailing_BAO_Mailing
::commonCompose($form);
238 CRM_Core_BAO_File
::buildAttachment($form, NULL);
240 if ($form->_single
) {
241 // also fix the user context stack
242 if ($form->_caseId
) {
243 $ccid = CRM_Core_DAO
::getFieldValue('CRM_Case_DAO_CaseContact', $form->_caseId
,
244 'contact_id', 'case_id'
246 $url = CRM_Utils_System
::url('civicrm/contact/view/case',
247 "&reset=1&action=view&cid={$ccid}&id={$form->_caseId}"
250 elseif ($form->_context
) {
251 $url = CRM_Utils_System
::url('civicrm/dashboard', 'reset=1');
254 $url = CRM_Utils_System
::url('civicrm/contact/view',
255 "&show=1&action=browse&cid={$form->_contactIds[0]}&selectedChild=activity"
259 $session = CRM_Core_Session
::singleton();
260 $session->replaceUserContext($url);
261 $form->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
264 $form->addDefaultButtons(ts('Send Email'), 'upload');
268 'followup_assignee_contact_id' => [
269 'type' => 'entityRef',
270 'label' => ts('Assigned to'),
274 'api' => ['params' => ['is_deceased' => 0]],
277 'followup_activity_type_id' => [
279 'label' => ts('Followup Activity'),
280 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant
::ActivityType(FALSE),
281 'extra' => ['class' => 'crm-select2'],
283 'followup_activity_subject' => [
285 'label' => ts('Subject'),
286 'attributes' => CRM_Core_DAO
::getAttribute('CRM_Activity_DAO_Activity',
293 $form->add('datepicker', 'followup_date', ts('in'));
295 foreach ($fields as $field => $values) {
296 if (!empty($fields[$field])) {
297 $attribute = $values['attributes'] ??
NULL;
298 $required = !empty($values['required']);
300 if ($values['type'] == 'select' && empty($attribute)) {
301 $form->addSelect($field, ['entity' => 'activity'], $required);
303 elseif ($values['type'] == 'entityRef') {
304 $form->addEntityRef($field, $values['label'], $attribute, $required);
307 $form->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array
::value('extra', $values));
312 //Added for CRM-15984: Add campaign field
313 CRM_Campaign_BAO_Campaign
::addCampaign($form);
315 $form->addFormRule(['CRM_Contact_Form_Task_EmailCommon', 'formRule'], $form);
316 CRM_Core_Resources
::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
322 * @param array $fields
323 * The input form values.
324 * @param array $dontCare
326 * Additional values form 'this'.
329 * true if no errors, else array of errors
331 public static function formRule($fields, $dontCare, $self) {
333 $template = CRM_Core_Smarty
::singleton();
335 if (isset($fields['html_message'])) {
336 $htmlMessage = str_replace(["\n", "\r"], ' ', $fields['html_message']);
337 $htmlMessage = str_replace('"', '\"', $htmlMessage);
338 $template->assign('htmlContent', $htmlMessage);
342 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
343 $errors['saveTemplateName'] = ts("Enter name to save message template");
346 return empty($errors) ?
TRUE : $errors;
350 * Process the form after the input has been submitted and validated.
352 * @param CRM_Core_Form $form
354 * @throws \CRM_Core_Exception
355 * @throws \CiviCRM_API3_Exception
356 * @throws \Civi\API\Exception\UnauthorizedException
358 public static function postProcess(&$form) {
359 self
::bounceIfSimpleMailLimitExceeded(count($form->_contactIds
));
361 // check and ensure that
362 $formValues = $form->controller
->exportValues($form->getName());
363 self
::submit($form, $formValues);
367 * Submit the form values.
369 * This is also accessible for testing.
371 * @param CRM_Core_Form $form
372 * @param array $formValues
374 * @throws \CRM_Core_Exception
375 * @throws \CiviCRM_API3_Exception
376 * @throws \Civi\API\Exception\UnauthorizedException
378 public static function submit(&$form, $formValues) {
379 self
::saveMessageTemplate($formValues);
381 $from = $formValues['from_email_address'] ??
NULL;
382 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
383 // If we have had a contact email used here the value returned from the line above will be the
384 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
385 $from = CRM_Utils_Mail
::formatFromAddress($from);
386 $subject = $formValues['subject'];
388 // CRM-13378: Append CC and BCC information at the end of Activity Details and format cc and bcc fields
389 $elements = ['cc_id', 'bcc_id'];
390 $additionalDetails = NULL;
391 $ccValues = $bccValues = [];
392 foreach ($elements as $element) {
393 if (!empty($formValues[$element])) {
394 $allEmails = explode(',', $formValues[$element]);
395 foreach ($allEmails as $value) {
396 list($contactId, $email) = explode('::', $value);
397 $contactURL = CRM_Utils_System
::url('civicrm/contact/view', "reset=1&force=1&cid={$contactId}", TRUE);
400 $ccValues['email'][] = '"' . $form->_contactDetails
[$contactId]['sort_name'] . '" <' . $email . '>';
401 $ccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails
[$contactId]['display_name'] . "</a>";
405 $bccValues['email'][] = '"' . $form->_contactDetails
[$contactId]['sort_name'] . '" <' . $email . '>';
406 $bccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails
[$contactId]['display_name'] . "</a>";
414 if (!empty($ccValues)) {
415 $cc = implode(',', $ccValues['email']);
416 $additionalDetails .= "\ncc : " . implode(", ", $ccValues['details']);
418 if (!empty($bccValues)) {
419 $bcc = implode(',', $bccValues['email']);
420 $additionalDetails .= "\nbcc : " . implode(", ", $bccValues['details']);
423 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
424 if (isset($form->_caseId
) && is_numeric($form->_caseId
)) {
425 $hash = substr(sha1(CIVICRM_SITE_KEY
. $form->_caseId
), 0, 7);
426 $subject = "[case #$hash] $subject";
430 CRM_Core_BAO_File
::formatAttachment($formValues,
435 // format contact details array to handle multiple emails from same contact
436 $formattedContactDetails = [];
438 foreach ($form->_contactIds
as $key => $contactId) {
439 // if we dont have details on this contactID, we should ignore
440 // potentially this is due to the contact not wanting to receive email
441 if (!isset($form->_contactDetails
[$contactId])) {
444 $email = $form->_toContactEmails
[$key];
445 // prevent duplicate emails if same email address is selected CRM-4067
446 // we should allow same emails for different contacts
447 $emailKey = "{$contactId}::{$email}";
448 if (!in_array($emailKey, $tempEmails)) {
449 $tempEmails[] = $emailKey;
450 $details = $form->_contactDetails
[$contactId];
451 $details['email'] = $email;
452 unset($details['email_id']);
453 $formattedContactDetails[] = $details;
457 $contributionIds = [];
458 if ($form->getVar('_contributionIds')) {
459 $contributionIds = $form->getVar('_contributionIds');
463 list($sent, $activityId) = CRM_Activity_BAO_Activity
::sendEmail(
464 $formattedContactDetails,
466 $formValues['text_message'],
467 $formValues['html_message'],
474 array_keys($form->_toContactDetails
),
477 CRM_Utils_Array
::value('campaign_id', $formValues),
478 $form->getVar('_caseId')
481 $followupStatus = '';
483 $followupActivity = NULL;
484 if (!empty($formValues['followup_activity_type_id'])) {
485 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
486 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
487 $params['followup_date'] = $formValues['followup_date'];
488 $params['target_contact_id'] = $form->_contactIds
;
489 $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']);
490 $followupActivity = CRM_Activity_BAO_Activity
::createFollowupActivity($activityId, $params);
491 $followupStatus = ts('A followup activity has been scheduled.');
493 if (Civi
::settings()->get('activity_assignee_notification')) {
494 if ($followupActivity) {
495 $mailToFollowupContacts = [];
496 $assignee = [$followupActivity->id
];
497 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment
::getAssigneeNames($assignee, TRUE, FALSE);
498 foreach ($assigneeContacts as $values) {
499 $mailToFollowupContacts[$values['email']] = $values;
502 $sentFollowup = CRM_Activity_BAO_Activity
::sendToAssignee($followupActivity, $mailToFollowupContacts);
504 $followupStatus .= '<br />' . ts("A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).");
510 $count_success = count($form->_toContactDetails
);
511 CRM_Core_Session
::setStatus(ts('One message was sent successfully. ', [
512 'plural' => '%count messages were sent successfully. ',
513 'count' => $count_success,
514 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
517 // Display the name and number of contacts for those email is not sent.
518 // php 5.4 throws out a notice since the values of these below arrays are arrays.
519 // the behavior is not documented in the php manual, but it does the right thing
520 // suppressing the notices to get things in good shape going forward
521 $emailsNotSent = @array_diff_assoc
($form->_allContactDetails
, $form->_contactDetails
);
523 if ($emailsNotSent) {
525 foreach ($emailsNotSent as $contactId => $values) {
526 $displayName = $values['display_name'];
527 $email = $values['email'];
528 $contactViewUrl = CRM_Utils_System
::url('civicrm/contact/view', "reset=1&cid=$contactId");
529 $not_sent[] = "<a href='$contactViewUrl' title='$email'>$displayName</a>" . ($values['on_hold'] ?
'(' . ts('on hold') . ')' : '');
531 $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>';
532 CRM_Core_Session
::setStatus($status, ts('One Message Not Sent', [
533 'count' => count($emailsNotSent),
534 'plural' => '%count Messages Not Sent',
538 if (isset($form->_caseId
)) {
539 // if case-id is found in the url, create case activity record
540 $cases = explode(',', $form->_caseId
);
541 foreach ($cases as $key => $val) {
542 if (is_numeric($val)) {
544 'activity_id' => $activityId,
547 CRM_Case_BAO_Case
::processCaseActivity($caseParams);
554 * Save the template if update selected.
556 * @param array $formValues
558 * @throws \CiviCRM_API3_Exception
559 * @throws \Civi\API\Exception\UnauthorizedException
561 protected static function saveMessageTemplate($formValues) {
562 if (!empty($formValues['saveTemplate']) ||
!empty($formValues['updateTemplate'])) {
564 'msg_text' => $formValues['text_message'],
565 'msg_html' => $formValues['html_message'],
566 'msg_subject' => $formValues['subject'],
570 if (!empty($formValues['saveTemplate'])) {
571 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
572 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
575 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
576 $messageTemplate['id'] = $formValues['template'];
577 unset($messageTemplate['msg_title']);
578 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
584 * Bounce if there are more emails than permitted.
587 * The number of emails the user is attempting to send
589 public static function bounceIfSimpleMailLimitExceeded($count) {
590 $limit = Civi
::settings()->get('simple_mail_limit');
591 if ($count > $limit) {
592 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.',
599 * Get the emails from the added element.
601 * @param HTML_QuickForm_Element $element
605 protected static function getEmails($element): array {
606 $allEmails = explode(',', $element->getValue());
608 foreach ($allEmails as $value) {
609 $values = explode('::', $value);
610 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];