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
21 * This class provides the common functionality for tasks that send emails.
23 trait CRM_Contact_Form_Task_EmailTrait
{
26 * Are we operating in "single mode", i.e. sending email to one
31 public $_single = FALSE;
33 public $_noEmails = FALSE;
36 * All the existing templates in the system.
43 * Store "to" contact details.
46 public $_toContactDetails = [];
49 * Store all selected contact id's, that includes to, cc and bcc contacts
52 public $_allContactIds = [];
55 * Store only "to" contact ids.
58 public $_toContactIds = [];
61 * Store only "cc" contact ids.
64 public $_ccContactIds = [];
67 * Store only "bcc" contact ids.
71 public $_bccContactIds = [];
74 * Is the form being loaded from a search action.
78 public $isSearchContext = TRUE;
80 public $contactEmails = [];
83 * Contacts form whom emails could not be sent.
85 * An array of contact ids and the relevant message details.
89 protected $suppressedEmails = [];
92 * Getter for isSearchContext.
96 public function isSearchContext(): bool {
97 return $this->isSearchContext
;
101 * Setter for isSearchContext.
103 * @param bool $isSearchContext
105 public function setIsSearchContext(bool $isSearchContext) {
106 $this->isSearchContext
= $isSearchContext;
110 * Build all the data structures needed to build the form.
112 * @throws \CiviCRM_API3_Exception
113 * @throws \CRM_Core_Exception
115 public function preProcess() {
116 $this->traitPreProcess();
120 * Call trait preProcess function.
122 * This function exists as a transitional arrangement so classes overriding
123 * preProcess can still call it. Ideally it will be melded into preProcess later.
125 * @throws \CiviCRM_API3_Exception
126 * @throws \CRM_Core_Exception
128 protected function traitPreProcess() {
129 CRM_Contact_Form_Task_EmailCommon
::preProcessFromAddress($this);
130 if ($this->isSearchContext()) {
131 // Currently only the contact email form is callable outside search context.
132 parent
::preProcess();
134 $this->setContactIDs();
135 $this->assign('single', $this->_single
);
136 if (CRM_Core_Permission
::check('administer CiviCRM')) {
137 $this->assign('isAdmin', 1);
142 * Build the form object.
144 * @throws \CRM_Core_Exception
146 public function buildQuickForm() {
147 // Suppress form might not be required but perhaps there was a risk some other process had set it to TRUE.
148 $this->assign('suppressForm', FALSE);
149 $this->assign('emailTask', TRUE);
152 $suppressedEmails = 0;
153 //here we are getting logged in user id as array but we need target contact id. CRM-5988
154 $cid = $this->get('cid');
156 $this->_contactIds
= explode(',', $cid);
158 if (count($this->_contactIds
) > 1) {
159 $this->_single
= FALSE;
161 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds
));
166 $to = $this->add('text', 'to', ts('To'), $emailAttributes, TRUE);
168 $this->addEntityRef('cc_id', ts('CC'), [
173 $this->addEntityRef('bcc_id', ts('BCC'), [
178 if ($to->getValue()) {
179 $this->_toContactIds
= $this->_contactIds
= [];
182 if (property_exists($this, '_context') && $this->_context
=== 'standalone') {
183 $setDefaults = FALSE;
186 $this->_allContactIds
= $this->_toContactIds
= $this->_contactIds
;
188 if ($to->getValue()) {
189 foreach ($this->getEmails($to) as $value) {
190 $contactId = $value['contact_id'];
191 $email = $value['email'];
193 $this->_contactIds
[] = $this->_toContactIds
[] = $contactId;
194 $this->_toContactEmails
[] = $email;
195 $this->_allContactIds
[] = $contactId;
201 //get the group of contacts as per selected by user in case of Find Activities
202 if (!empty($this->_activityHolderIds
)) {
203 $contact = $this->get('contacts');
204 $this->_allContactIds
= $this->_contactIds
= $contact;
207 // check if we need to setdefaults and check for valid contact emails / communication preferences
208 if (is_array($this->_allContactIds
) && $setDefaults) {
209 // get the details for all selected contacts ( to, cc and bcc contacts )
210 $allContactDetails = civicrm_api3('Contact', 'get', [
211 'id' => ['IN' => $this->_allContactIds
],
212 'return' => ['sort_name', 'email', 'do_not_email', 'is_deceased', 'on_hold', 'display_name', 'preferred_mail_format'],
213 'options' => ['limit' => 0],
216 // The contact task supports passing in email_id in a url. It supports a single email
217 // and is marked as having been related to CiviHR.
218 // The array will look like $this->_toEmail = ['email' => 'x', 'contact_id' => 2])
219 // If it exists we want to use the specified email which might be different to the primary email
221 if (!empty($this->_toEmail
['contact_id']) && !empty($allContactDetails[$this->_toEmail
['contact_id']])) {
222 $allContactDetails[$this->_toEmail
['contact_id']]['email'] = $this->_toEmail
['email'];
225 // perform all validations on unique contact Ids
226 foreach ($allContactDetails as $contactId => $value) {
227 if ($value['do_not_email'] ||
empty($value['email']) ||
!empty($value['is_deceased']) ||
$value['on_hold']) {
228 $this->setSuppressedEmail($contactId, $value);
230 elseif (in_array($contactId, $this->_toContactIds
)) {
231 $this->_toContactDetails
[$contactId] = $this->_contactDetails
[$contactId] = $value;
233 'text' => '"' . $value['sort_name'] . '" <' . $value['email'] . '>',
234 'id' => "$contactId::{$value['email']}",
239 if (empty($toArray)) {
240 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.'));
244 $this->assign('toContact', json_encode($toArray));
246 $this->assign('suppressedEmails', count($this->suppressedEmails
));
248 $this->assign('totalSelectedContacts', count($this->_contactIds
));
250 $this->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE);
252 $this->add('select', 'from_email_address', ts('From'), $this->_fromEmails
, TRUE);
254 CRM_Mailing_BAO_Mailing
::commonCompose($this);
257 CRM_Core_BAO_File
::buildAttachment($this, NULL);
259 if ($this->_single
) {
260 // also fix the user context stack
261 if ($this->_caseId
) {
262 $ccid = CRM_Core_DAO
::getFieldValue('CRM_Case_DAO_CaseContact', $this->_caseId
,
263 'contact_id', 'case_id'
265 $url = CRM_Utils_System
::url('civicrm/contact/view/case',
266 "&reset=1&action=view&cid={$ccid}&id={$this->_caseId}"
269 elseif ($this->_context
) {
270 $url = CRM_Utils_System
::url('civicrm/dashboard', 'reset=1');
273 $url = CRM_Utils_System
::url('civicrm/contact/view',
274 "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity"
278 $session = CRM_Core_Session
::singleton();
279 $session->replaceUserContext($url);
280 $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
283 $this->addDefaultButtons(ts('Send Email'), 'upload');
287 'followup_assignee_contact_id' => [
288 'type' => 'entityRef',
289 'label' => ts('Assigned to'),
293 'api' => ['params' => ['is_deceased' => 0]],
296 'followup_activity_type_id' => [
298 'label' => ts('Followup Activity'),
299 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant
::ActivityType(FALSE),
300 'extra' => ['class' => 'crm-select2'],
302 'followup_activity_subject' => [
304 'label' => ts('Subject'),
305 'attributes' => CRM_Core_DAO
::getAttribute('CRM_Activity_DAO_Activity',
312 $this->add('datepicker', 'followup_date', ts('in'));
314 foreach ($fields as $field => $values) {
315 if (!empty($fields[$field])) {
316 $attribute = $values['attributes'] ??
NULL;
317 $required = !empty($values['required']);
319 if ($values['type'] === 'select' && empty($attribute)) {
320 $this->addSelect($field, ['entity' => 'activity'], $required);
322 elseif ($values['type'] === 'entityRef') {
323 $this->addEntityRef($field, $values['label'], $attribute, $required);
326 $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array
::value('extra', $values));
331 //Added for CRM-15984: Add campaign field
332 CRM_Campaign_BAO_Campaign
::addCampaign($this);
334 $this->addFormRule(['CRM_Contact_Form_Task_EmailCommon', 'formRule'], $this);
335 CRM_Core_Resources
::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
339 * Process the form after the input has been submitted and validated.
341 * @throws \CRM_Core_Exception
342 * @throws \CiviCRM_API3_Exception
343 * @throws \Civi\API\Exception\UnauthorizedException
344 * @throws \API_Exception
346 public function postProcess() {
347 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds
));
349 // check and ensure that
350 $formValues = $this->controller
->exportValues($this->getName());
351 $this->submit($formValues);
355 * Bounce if there are more emails than permitted.
358 * The number of emails the user is attempting to send
360 protected function bounceIfSimpleMailLimitExceeded($count) {
361 $limit = Civi
::settings()->get('simple_mail_limit');
362 if ($count > $limit) {
363 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.',
370 * Submit the form values.
372 * This is also accessible for testing.
374 * @param array $formValues
376 * @throws \CRM_Core_Exception
377 * @throws \CiviCRM_API3_Exception
378 * @throws \Civi\API\Exception\UnauthorizedException
379 * @throws \API_Exception
381 public function submit($formValues) {
382 $this->saveMessageTemplate($formValues);
384 $from = $formValues['from_email_address'] ??
NULL;
385 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
386 // If we have had a contact email used here the value returned from the line above will be the
387 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
388 $from = CRM_Utils_Mail
::formatFromAddress($from);
390 $ccArray = $formValues['cc_id'] ?
explode(',', $formValues['cc_id']) : [];
391 $cc = $this->getEmailString($ccArray);
392 $additionalDetails = empty($ccArray) ?
'' : "\ncc : " . $this->getEmailUrlString($ccArray);
394 $bccArray = $formValues['bcc_id'] ?
explode(',', $formValues['bcc_id']) : [];
395 $bcc = $this->getEmailString($bccArray);
396 $additionalDetails .= empty($bccArray) ?
'' : "\nbcc : " . $this->getEmailUrlString($bccArray);
398 // format contact details array to handle multiple emails from same contact
399 $formattedContactDetails = [];
400 foreach ($this->_contactIds
as $key => $contactId) {
401 // if we dont have details on this contactID, we should ignore
402 // potentially this is due to the contact not wanting to receive email
403 if (!isset($this->_contactDetails
[$contactId])) {
406 $email = $this->_toContactEmails
[$key];
407 // prevent duplicate emails if same email address is selected CRM-4067
408 // we should allow same emails for different contacts
409 $details = $this->_contactDetails
[$contactId];
410 $details['email'] = $email;
411 unset($details['email_id']);
412 $formattedContactDetails["{$contactId}::{$email}"] = $details;
416 list($sent, $activityId) = CRM_Activity_BAO_Activity
::sendEmail(
417 $formattedContactDetails,
418 $this->getSubject($formValues['subject']),
419 $formValues['text_message'],
420 $formValues['html_message'],
424 $this->getAttachments($formValues),
427 array_keys($this->_toContactDetails
),
429 $this->getVar('_contributionIds') ??
[],
430 CRM_Utils_Array
::value('campaign_id', $formValues),
431 $this->getVar('_caseId')
435 $followupStatus = $this->createFollowUpActivities($formValues, $activityId);
436 $count_success = count($this->_toContactDetails
);
437 CRM_Core_Session
::setStatus(ts('One message was sent successfully. ', [
438 'plural' => '%count messages were sent successfully. ',
439 'count' => $count_success,
440 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
443 if (!empty($this->suppressedEmails
)) {
444 $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>', $this->suppressedEmails
) . '</li></ul>';
445 CRM_Core_Session
::setStatus($status, ts('One Message Not Sent', [
446 'count' => count($this->suppressedEmails
),
447 'plural' => '%count Messages Not Sent',
451 if (isset($this->_caseId
)) {
452 // if case-id is found in the url, create case activity record
453 $cases = explode(',', $this->_caseId
);
454 foreach ($cases as $key => $val) {
455 if (is_numeric($val)) {
457 'activity_id' => $activityId,
460 CRM_Case_BAO_Case
::processCaseActivity($caseParams);
467 * Save the template if update selected.
469 * @param array $formValues
471 * @throws \CiviCRM_API3_Exception
472 * @throws \Civi\API\Exception\UnauthorizedException
474 protected function saveMessageTemplate($formValues) {
475 if (!empty($formValues['saveTemplate']) ||
!empty($formValues['updateTemplate'])) {
477 'msg_text' => $formValues['text_message'],
478 'msg_html' => $formValues['html_message'],
479 'msg_subject' => $formValues['subject'],
483 if (!empty($formValues['saveTemplate'])) {
484 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
485 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
488 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
489 $messageTemplate['id'] = $formValues['template'];
490 unset($messageTemplate['msg_title']);
491 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
497 * List available tokens for this form.
501 public function listTokens() {
502 return CRM_Core_SelectValues
::contactTokens();
506 * Get the emails from the added element.
508 * @param HTML_QuickForm_Element $element
512 protected function getEmails($element): array {
513 $allEmails = explode(',', $element->getValue());
515 foreach ($allEmails as $value) {
516 $values = explode('::', $value);
517 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];
523 * Get the string for the email IDs.
525 * @param array $emailIDs
526 * Array of email IDs.
529 * e.g. "Smith, Bob<bob.smith@example.com>".
531 * @throws \API_Exception
532 * @throws \Civi\API\Exception\UnauthorizedException
534 protected function getEmailString(array $emailIDs): string {
535 if (empty($emailIDs)) {
538 $emails = Email
::get()
539 ->addWhere('id', 'IN', $emailIDs)
540 ->setCheckPermissions(FALSE)
541 ->setSelect(['contact_id', 'email', 'contact.sort_name', 'contact.display_name'])->execute();
543 foreach ($emails as $email) {
544 $this->contactEmails
[$email['id']] = $email;
545 $emailStrings[] = '"' . $email['contact.sort_name'] . '" <' . $email['email'] . '>';
547 return implode(',', $emailStrings);
551 * Get the url string.
553 * This is called after the contacts have been retrieved so we don't need to re-retrieve.
555 * @param array $emailIDs
558 * e.g. <a href='{$contactURL}'>Bob Smith</a>'
560 protected function getEmailUrlString(array $emailIDs): string {
562 foreach ($emailIDs as $email) {
563 $contactURL = CRM_Utils_System
::url('civicrm/contact/view', ['reset' => 1, 'force' => 1, 'cid' => $this->contactEmails
[$email]['contact_id']], TRUE);
564 $urlString .= "<a href='{$contactURL}'>" . $this->contactEmails
[$email]['contact.display_name'] . '</a>';
570 * Set the emails that are not to be sent out.
572 * @param int $contactID
573 * @param array $values
575 protected function setSuppressedEmail($contactID, $values) {
576 $contactViewUrl = CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $contactID);
577 $this->suppressedEmails
[$contactID] = "<a href='$contactViewUrl' title='{$values['email']}'>{$values['display_name']}</a>" . ($values['on_hold'] ?
'(' . ts('on hold') . ')' : '');
581 * Get any attachments.
583 * @param array $formValues
587 protected function getAttachments(array $formValues): array {
589 CRM_Core_BAO_File
::formatAttachment($formValues,
597 * Get the subject for the message.
599 * The case handling should possibly be on the case form.....
601 * @param string $subject
605 protected function getSubject(string $subject):string {
606 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
607 if (isset($this->_caseId
) && is_numeric($this->_caseId
)) {
608 $hash = substr(sha1(CIVICRM_SITE_KEY
. $this->_caseId
), 0, 7);
609 $subject = "[case #$hash] $subject";
615 * Create any follow up activities.
617 * @param array $formValues
618 * @param int $activityId
622 * @throws \CRM_Core_Exception
624 protected function createFollowUpActivities($formValues, $activityId): string {
626 $followupStatus = '';
627 $followupActivity = NULL;
628 if (!empty($formValues['followup_activity_type_id'])) {
629 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
630 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
631 $params['followup_date'] = $formValues['followup_date'];
632 $params['target_contact_id'] = $this->_contactIds
;
633 $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']);
634 $followupActivity = CRM_Activity_BAO_Activity
::createFollowupActivity($activityId, $params);
635 $followupStatus = ts('A followup activity has been scheduled.');
637 if (Civi
::settings()->get('activity_assignee_notification')) {
638 if ($followupActivity) {
639 $mailToFollowupContacts = [];
640 $assignee = [$followupActivity->id
];
641 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment
::getAssigneeNames($assignee, TRUE, FALSE);
642 foreach ($assigneeContacts as $values) {
643 $mailToFollowupContacts[$values['email']] = $values;
646 $sentFollowup = CRM_Activity_BAO_Activity
::sendToAssignee($followupActivity, $mailToFollowupContacts);
648 $followupStatus .= '<br />' . ts('A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).');
653 return $followupStatus;