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;
34 * All the existing templates in the system.
41 * Email addresses to send to.
45 protected $emails = [];
48 * Store "to" contact details.
51 public $_toContactDetails = [];
54 * Store all selected contact id's, that includes to, cc and bcc contacts
57 public $_allContactIds = [];
60 * Store only "to" contact ids.
63 public $_toContactIds = [];
66 * Is the form being loaded from a search action.
70 public $isSearchContext = TRUE;
72 public $contactEmails = [];
75 * Contacts form whom emails could not be sent.
77 * An array of contact ids and the relevant message details.
81 protected $suppressedEmails = [];
84 * Getter for isSearchContext.
88 public function isSearchContext(): bool {
89 return $this->isSearchContext
;
93 * Setter for isSearchContext.
95 * @param bool $isSearchContext
97 public function setIsSearchContext(bool $isSearchContext) {
98 $this->isSearchContext
= $isSearchContext;
102 * Build all the data structures needed to build the form.
104 * @throws \CiviCRM_API3_Exception
105 * @throws \CRM_Core_Exception
107 public function preProcess() {
108 $this->traitPreProcess();
112 * Call trait preProcess function.
114 * This function exists as a transitional arrangement so classes overriding
115 * preProcess can still call it. Ideally it will be melded into preProcess
118 * @throws \CRM_Core_Exception
119 * @throws \API_Exception
121 protected function traitPreProcess() {
122 if ($this->isSearchContext()) {
123 // Currently only the contact email form is callable outside search context.
124 parent
::preProcess();
126 $this->setContactIDs();
127 $this->assign('single', $this->_single
);
128 if (CRM_Core_Permission
::check('administer CiviCRM')) {
129 $this->assign('isAdmin', 1);
134 * Build the form object.
136 * @throws \CRM_Core_Exception
138 public function buildQuickForm() {
139 // Suppress form might not be required but perhaps there was a risk some other process had set it to TRUE.
140 $this->assign('suppressForm', FALSE);
141 $this->assign('emailTask', TRUE);
144 $suppressedEmails = 0;
145 //here we are getting logged in user id as array but we need target contact id. CRM-5988
146 $cid = $this->get('cid');
148 $this->_contactIds
= explode(',', $cid);
150 // The default in CRM_Core_Form_Task is null, but changing it there gives
152 if (is_null($this->_contactIds
)) {
153 $this->_contactIds
= [];
155 if (count($this->_contactIds
) > 1) {
156 $this->_single
= FALSE;
158 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds
));
163 $this->add('text', 'to', ts('To'), $emailAttributes, TRUE);
165 $this->addEntityRef('cc_id', ts('CC'), [
170 $this->addEntityRef('bcc_id', ts('BCC'), [
176 if (property_exists($this, '_context') && $this->_context
=== 'standalone') {
177 $setDefaults = FALSE;
180 $this->_allContactIds
= $this->_toContactIds
= $this->_contactIds
;
182 //get the group of contacts as per selected by user in case of Find Activities
183 if (!empty($this->_activityHolderIds
)) {
184 $contact = $this->get('contacts');
185 $this->_allContactIds
= $this->_contactIds
= $contact;
188 // check if we need to setdefaults and check for valid contact emails / communication preferences
189 if (!empty($this->_allContactIds
) && $setDefaults) {
190 // get the details for all selected contacts ( to, cc and bcc contacts )
191 $allContactDetails = civicrm_api3('Contact', 'get', [
192 'id' => ['IN' => $this->_allContactIds
],
193 'return' => ['sort_name', 'email', 'do_not_email', 'is_deceased', 'on_hold', 'display_name', 'preferred_mail_format'],
194 'options' => ['limit' => 0],
197 // The contact task supports passing in email_id in a url. It supports a single email
198 // and is marked as having been related to CiviHR.
199 // The array will look like $this->_toEmail = ['email' => 'x', 'contact_id' => 2])
200 // If it exists we want to use the specified email which might be different to the primary email
202 if (!empty($this->_toEmail
['contact_id']) && !empty($allContactDetails[$this->_toEmail
['contact_id']])) {
203 $allContactDetails[$this->_toEmail
['contact_id']]['email'] = $this->_toEmail
['email'];
206 // perform all validations on unique contact Ids
207 foreach ($allContactDetails as $contactId => $value) {
208 if ($value['do_not_email'] ||
empty($value['email']) ||
!empty($value['is_deceased']) ||
$value['on_hold']) {
209 $this->setSuppressedEmail($contactId, $value);
211 elseif (in_array($contactId, $this->_toContactIds
)) {
212 $this->_toContactDetails
[$contactId] = $this->_contactDetails
[$contactId] = $value;
214 'text' => '"' . $value['sort_name'] . '" <' . $value['email'] . '>',
215 'id' => "$contactId::{$value['email']}",
220 if (empty($toArray)) {
221 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.'));
225 $this->assign('toContact', json_encode($toArray));
227 $this->assign('suppressedEmails', count($this->suppressedEmails
));
229 $this->assign('totalSelectedContacts', count($this->_contactIds
));
231 $this->add('text', 'subject', ts('Subject'), ['size' => 50, 'maxlength' => 254], TRUE);
233 $this->add('select', 'from_email_address', ts('From'), $this->getFromEmails(), TRUE);
235 CRM_Mailing_BAO_Mailing
::commonCompose($this);
238 CRM_Core_BAO_File
::buildAttachment($this, NULL);
240 if ($this->_single
) {
241 CRM_Core_Session
::singleton()->replaceUserContext($this->getRedirectUrl());
243 $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
246 'followup_assignee_contact_id' => [
247 'type' => 'entityRef',
248 'label' => ts('Assigned to'),
252 'api' => ['params' => ['is_deceased' => 0]],
255 'followup_activity_type_id' => [
257 'label' => ts('Followup Activity'),
258 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant
::ActivityType(FALSE),
259 'extra' => ['class' => 'crm-select2'],
261 'followup_activity_subject' => [
263 'label' => ts('Subject'),
264 'attributes' => CRM_Core_DAO
::getAttribute('CRM_Activity_DAO_Activity',
271 $this->add('datepicker', 'followup_date', ts('in'));
273 foreach ($fields as $field => $values) {
274 if (!empty($fields[$field])) {
275 $attribute = $values['attributes'] ??
NULL;
276 $required = !empty($values['required']);
278 if ($values['type'] === 'select' && empty($attribute)) {
279 $this->addSelect($field, ['entity' => 'activity'], $required);
281 elseif ($values['type'] === 'entityRef') {
282 $this->addEntityRef($field, $values['label'], $attribute, $required);
285 $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array
::value('extra', $values));
290 //Added for CRM-15984: Add campaign field
291 CRM_Campaign_BAO_Campaign
::addCampaign($this);
293 $this->addFormRule([__CLASS__
, 'saveTemplateFormRule'], $this);
294 $this->addFormRule([__CLASS__
, 'deprecatedTokensFormRule'], $this);
295 CRM_Core_Resources
::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
299 * Set relevant default values.
303 * @throws \API_Exception
304 * @throws \CRM_Core_Exception
306 public function setDefaultValues(): array {
307 $defaults = parent
::setDefaultValues();
308 $fromEmails = $this->getFromEmails();
309 if (is_numeric(key($fromEmails))) {
310 $emailID = (int) key($fromEmails);
311 $defaults = CRM_Core_BAO_Email
::getEmailSignatureDefaults($emailID);
313 if (!Civi
::settings()->get('allow_mail_from_logged_in_contact')) {
314 $defaults['from_email_address'] = current(CRM_Core_BAO_Domain
::getNameAndEmail(FALSE, TRUE));
320 * Process the form after the input has been submitted and validated.
322 * @throws \CRM_Core_Exception
323 * @throws \CiviCRM_API3_Exception
324 * @throws \Civi\API\Exception\UnauthorizedException
325 * @throws \API_Exception
327 public function postProcess() {
328 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds
));
329 $formValues = $this->controller
->exportValues($this->getName());
330 $this->submit($formValues);
334 * Bounce if there are more emails than permitted.
337 * The number of emails the user is attempting to send
339 protected function bounceIfSimpleMailLimitExceeded($count): void
{
340 $limit = Civi
::settings()->get('simple_mail_limit');
341 if ($count > $limit) {
342 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.',
349 * Submit the form values.
351 * This is also accessible for testing.
353 * @param array $formValues
355 * @throws \CRM_Core_Exception
356 * @throws \CiviCRM_API3_Exception
357 * @throws \Civi\API\Exception\UnauthorizedException
358 * @throws \API_Exception
360 public function submit($formValues): void
{
361 $this->saveMessageTemplate($formValues);
362 $from = $formValues['from_email_address'];
363 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
364 // If we have had a contact email used here the value returned from the line above will be the
365 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
366 $from = CRM_Utils_Mail
::formatFromAddress($from);
368 $ccArray = $formValues['cc_id'] ?
explode(',', $formValues['cc_id']) : [];
369 $cc = $this->getEmailString($ccArray);
370 $additionalDetails = empty($ccArray) ?
'' : "\ncc : " . $this->getEmailUrlString($ccArray);
372 $bccArray = $formValues['bcc_id'] ?
explode(',', $formValues['bcc_id']) : [];
373 $bcc = $this->getEmailString($bccArray);
374 $additionalDetails .= empty($bccArray) ?
'' : "\nbcc : " . $this->getEmailUrlString($bccArray);
377 [$sent, $activityIds] = $this->sendEmail(
378 $formValues['text_message'],
379 $formValues['html_message'],
381 $this->getAttachments($formValues),
385 CRM_Utils_Array
::value('campaign_id', $formValues),
390 // Only use the first activity id if there's multiple.
391 // If there's multiple recipients the idea behind multiple activities
392 // is to record the token value replacements separately, but that
393 // has no meaning for followup activities, and this doesn't prevent
394 // creating more manually if desired.
395 $followupStatus = $this->createFollowUpActivities($formValues, $activityIds[0]);
396 $count_success = count($this->_toContactDetails
);
397 CRM_Core_Session
::setStatus(ts('One message was sent successfully. ', [
398 'plural' => '%count messages were sent successfully. ',
399 'count' => $count_success,
400 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
403 if (!empty($this->suppressedEmails
)) {
404 $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>';
405 CRM_Core_Session
::setStatus($status, ts('One Message Not Sent', [
406 'count' => count($this->suppressedEmails
),
407 'plural' => '%count Messages Not Sent',
413 * Save the template if update selected.
415 * @param array $formValues
417 * @throws \CiviCRM_API3_Exception
418 * @throws \Civi\API\Exception\UnauthorizedException
420 protected function saveMessageTemplate($formValues) {
421 if (!empty($formValues['saveTemplate']) ||
!empty($formValues['updateTemplate'])) {
423 'msg_text' => $formValues['text_message'],
424 'msg_html' => $formValues['html_message'],
425 'msg_subject' => $formValues['subject'],
429 if (!empty($formValues['saveTemplate'])) {
430 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
431 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
434 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
435 $messageTemplate['id'] = $formValues['template'];
436 unset($messageTemplate['msg_title']);
437 CRM_Core_BAO_MessageTemplate
::add($messageTemplate);
443 * Get the emails from the added element.
447 protected function getEmails(): array {
448 $allEmails = explode(',', $this->getSubmittedValue('to'));
450 foreach ($allEmails as $value) {
451 $values = explode('::', $value);
452 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];
458 * Get the string for the email IDs.
460 * @param array $emailIDs
461 * Array of email IDs.
464 * e.g. "Smith, Bob<bob.smith@example.com>".
466 * @throws \API_Exception
467 * @throws \Civi\API\Exception\UnauthorizedException
469 protected function getEmailString(array $emailIDs): string {
470 if (empty($emailIDs)) {
473 $emails = Email
::get()
474 ->addWhere('id', 'IN', $emailIDs)
475 ->setCheckPermissions(FALSE)
476 ->setSelect(['contact_id', 'email', 'contact_id.sort_name', 'contact_id.display_name'])->execute();
478 foreach ($emails as $email) {
479 $this->contactEmails
[$email['id']] = $email;
480 $emailStrings[] = '"' . $email['contact_id.sort_name'] . '" <' . $email['email'] . '>';
482 return implode(',', $emailStrings);
486 * Get the url string.
488 * This is called after the contacts have been retrieved so we don't need to re-retrieve.
490 * @param array $emailIDs
493 * e.g. <a href='{$contactURL}'>Bob Smith</a>'
495 protected function getEmailUrlString(array $emailIDs): string {
497 foreach ($emailIDs as $email) {
498 $contactURL = CRM_Utils_System
::url('civicrm/contact/view', ['reset' => 1, 'cid' => $this->contactEmails
[$email]['contact_id']], TRUE);
499 $urls[] = "<a href='{$contactURL}'>" . $this->contactEmails
[$email]['contact_id.display_name'] . '</a>';
501 return implode(', ', $urls);
505 * Set the emails that are not to be sent out.
507 * @param int $contactID
508 * @param array $values
510 protected function setSuppressedEmail($contactID, $values) {
511 $contactViewUrl = CRM_Utils_System
::url('civicrm/contact/view', 'reset=1&cid=' . $contactID);
512 $this->suppressedEmails
[$contactID] = "<a href='$contactViewUrl' title='{$values['email']}'>{$values['display_name']}</a>" . ($values['on_hold'] ?
'(' . ts('on hold') . ')' : '');
516 * Get any attachments.
518 * @param array $formValues
522 protected function getAttachments(array $formValues): array {
524 CRM_Core_BAO_File
::formatAttachment($formValues,
532 * Get the subject for the message.
536 protected function getSubject():string {
537 return (string) $this->getSubmittedValue('subject');
541 * Create any follow up activities.
543 * @param array $formValues
544 * @param int $activityId
548 * @throws \CRM_Core_Exception
550 protected function createFollowUpActivities($formValues, $activityId): string {
552 $followupStatus = '';
553 $followupActivity = NULL;
554 if (!empty($formValues['followup_activity_type_id'])) {
555 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
556 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
557 $params['followup_date'] = $formValues['followup_date'];
558 $params['target_contact_id'] = $this->_contactIds
;
559 $params['followup_assignee_contact_id'] = array_filter(explode(',', $formValues['followup_assignee_contact_id']));
560 $followupActivity = CRM_Activity_BAO_Activity
::createFollowupActivity($activityId, $params);
561 $followupStatus = ts('A followup activity has been scheduled.');
563 if (Civi
::settings()->get('activity_assignee_notification')) {
564 if ($followupActivity) {
565 $mailToFollowupContacts = [];
566 $assignee = [$followupActivity->id
];
567 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment
::getAssigneeNames($assignee, TRUE, FALSE);
568 foreach ($assigneeContacts as $values) {
569 $mailToFollowupContacts[$values['email']] = $values;
572 $sentFollowup = CRM_Activity_BAO_Activity
::sendToAssignee($followupActivity, $mailToFollowupContacts);
574 $followupStatus .= '<br />' . ts('A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).');
579 return $followupStatus;
585 * @param array $fields
586 * The input form values.
589 * true if no errors, else array of errors
591 public static function saveTemplateFormRule(array $fields) {
594 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
595 $errors['saveTemplateName'] = ts('Enter name to save message template');
597 return empty($errors) ?
TRUE : $errors;
601 * Prevent submission of deprecated tokens.
603 * Note this rule can be removed after a transition period.
604 * It's mostly to help to ensure users don't get missing tokens
605 * or unexpected output after the 5.43 upgrade until any
606 * old templates have aged out.
608 * @param array $fields
610 * @return bool|string[]
612 public static function deprecatedTokensFormRule(array $fields) {
613 $deprecatedTokens = [
614 '{case.status_id}' => '{case.status_id:label}',
615 '{case.case_type_id}' => '{case.case_type_id:label}',
616 '{contribution.campaign}' => '{contribution.campaign_id:label}',
617 '{contribution.payment_instrument}' => '{contribution.payment_instrument_id:label}',
618 '{contribution.contribution_id}' => '{contribution.id}',
619 '{contribution.contribution_source}' => '{contribution.source}',
620 '{contribution.contribution_status}' => '{contribution.contribution_status_id:label}',
621 '{contribution.contribution_cancel_date}' => '{contribution.cancel_date}',
622 '{contribution.type}' => '{contribution.financial_type_id:label}',
623 '{contribution.contribution_page_id}' => '{contribution.contribution_page_id:label}',
626 foreach ($deprecatedTokens as $token => $replacement) {
627 if (strpos($fields['html_message'], $token) !== FALSE) {
628 $tokenErrors[] = ts('Token %1 is no longer supported - use %2 instead', [$token, $replacement]);
631 return empty($tokenErrors) ?
TRUE : ['html_message' => implode('<br>', $tokenErrors)];
635 * Get selected contribution IDs.
639 protected function getContributionIDs(): array {
644 * Get case ID - if any.
648 * @throws \CRM_Core_Exception
650 protected function getCaseID(): ?
int {
651 $caseID = CRM_Utils_Request
::retrieve('caseid', 'String', $this);
653 return (int) $caseID;
661 protected function getFromEmails(): array {
662 $fromEmailValues = CRM_Core_BAO_Email
::getFromEmail();
664 if (empty($fromEmailValues)) {
665 CRM_Core_Error
::statusBounce(ts('Your user record does not have a valid email address and no from addresses have been configured.'));
667 return $fromEmailValues;
671 * Get the relevant emails.
677 protected function getEmail(int $index): string {
678 if (empty($this->emails
)) {
679 $toEmails = explode(',', $this->getSubmittedValue('to'));
680 foreach ($toEmails as $value) {
681 $parts = explode('::', $value);
682 $this->emails
[] = $parts[1];
685 return $this->emails
[$index];
689 * Send the message to all the contacts.
691 * Do not use this function outside of core tested code. It will change.
693 * It will also become protected once tests no longer call it.
697 * Also insert a contact activity in each contacts record.
701 * @param string $from
702 * @param array|null $attachments
703 * The array of attachments if any.
704 * @param string|null $cc
706 * @param string|null $bcc
708 * @param string|null $additionalDetails
709 * The additional information of CC and BCC appended to the activity Details.
710 * @param int|null $campaignId
711 * @param int|null $caseId
714 * bool $sent FIXME: this only indicates the status of the last email sent.
715 * array $activityIds The activity ids created, one per "To" recipient.
717 * @throws \API_Exception
718 * @throws \CRM_Core_Exception
719 * @throws \PEAR_Exception
722 * Also insert a contact activity in each contacts record.
726 * Also insert a contact activity in each contacts record.
728 public function sendEmail(
735 $additionalDetails = NULL,
740 $userID = CRM_Core_Session
::getLoggedInContactID();
742 $sent = $notSent = [];
743 $attachmentFileIds = [];
745 $firstActivityCreated = FALSE;
746 foreach ($this->getRowsForEmails() as $values) {
747 $contactId = $values['contact_id'];
748 $emailAddress = $values['email'];
749 $renderedTemplate = CRM_Core_BAO_MessageTemplate
::renderTemplate([
750 'messageTemplate' => [
753 'msg_subject' => $this->getSubject(),
755 'tokenContext' => array_merge(['schema' => $this->getTokenSchema()], ($values['schema'] ??
[])),
756 'contactId' => $contactId,
757 'disableSmarty' => !CRM_Utils_Constant
::value('CIVICRM_MAIL_SMARTY'),
761 // To minimize storage requirements, only one copy of any file attachments uploaded to CiviCRM is kept,
762 // even when multiple contacts will receive separate emails from CiviCRM.
763 if (!empty($attachmentFileIds)) {
764 $attachments = array_replace_recursive($attachments, $attachmentFileIds);
767 // Create email activity.
768 $activityID = $this->createEmailActivity($userID, $renderedTemplate['subject'], $renderedTemplate['html'], $renderedTemplate['text'], $additionalDetails, $campaignId, $attachments, $caseId);
769 $activityIds[] = $activityID;
771 if ($firstActivityCreated == FALSE && !empty($attachments)) {
772 $attachmentFileIds = CRM_Activity_BAO_Activity
::getAttachmentFileIds($activityID, $attachments);
773 $firstActivityCreated = TRUE;
776 if ($this->sendMessage(
779 $renderedTemplate['subject'],
780 $renderedTemplate['text'],
781 $renderedTemplate['html'],
784 // get the set of attachments from where they are stored
785 CRM_Core_BAO_File
::getEntityFile('civicrm_activity', $activityID),
794 return [$sent, $activityIds];
798 * @param int $sourceContactID
799 * The contact ID of the email "from".
800 * @param string $subject
801 * @param string $html
802 * @param string $text
803 * @param string $additionalDetails
804 * The additional information of CC and BCC appended to the activity details.
805 * @param int $campaignID
806 * @param array $attachments
810 * The created activity ID
811 * @throws \CRM_Core_Exception
813 protected function createEmailActivity($sourceContactID, $subject, $html, $text, $additionalDetails, $campaignID, $attachments, $caseID) {
814 $activityTypeID = CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'activity_type_id', 'Email');
816 // CRM-6265: save both text and HTML parts in details (if present)
817 if ($html and $text) {
818 $details = "-ALTERNATIVE ITEM 0-\n{$html}{$additionalDetails}\n-ALTERNATIVE ITEM 1-\n{$text}{$additionalDetails}\n-ALTERNATIVE END-\n";
821 $details = $html ?
$html : $text;
822 $details .= $additionalDetails;
826 'source_contact_id' => $sourceContactID,
827 'activity_type_id' => $activityTypeID,
828 'activity_date_time' => date('YmdHis'),
829 'subject' => $subject,
830 'details' => $details,
831 'status_id' => CRM_Core_PseudoConstant
::getKey('CRM_Activity_BAO_Activity', 'status_id', 'Completed'),
832 'campaign_id' => $campaignID,
834 if (!empty($caseID)) {
835 $activityParams['case_id'] = $caseID;
838 // CRM-5916: strip [case #…] before saving the activity (if present in subject)
839 $activityParams['subject'] = preg_replace('/\[case #([0-9a-h]{7})\] /', '', $activityParams['subject']);
841 // add the attachments to activity params here
843 // first process them
844 $activityParams = array_merge($activityParams, $attachments);
847 $activity = civicrm_api3('Activity', 'create', $activityParams);
849 return $activity['id'];
853 * Send message - under refactor.
858 * @param $text_message
859 * @param $html_message
860 * @param $emailAddress
862 * @param null $attachments
867 * @throws \CRM_Core_Exception
868 * @throws \PEAR_Exception
870 protected function sendMessage(
882 [$toDisplayName, $toEmail, $toDoNotEmail] = CRM_Contact_BAO_Contact
::getContactDetails($toID);
884 $toEmail = trim($emailAddress);
887 // make sure both email addresses are valid
888 // and that the recipient wants to receive email
889 if (empty($toEmail) or $toDoNotEmail) {
892 if (!trim($toDisplayName)) {
893 $toDisplayName = $toEmail;
896 $activityContacts = CRM_Activity_BAO_ActivityContact
::buildOptions('record_type_id', 'validate');
897 $targetID = CRM_Utils_Array
::key('Activity Targets', $activityContacts);
899 // create the params array
901 'groupName' => 'Activity Email Sender',
903 'toName' => $toDisplayName,
904 'toEmail' => $toEmail,
905 'subject' => $subject,
908 'text' => $text_message,
909 'html' => $html_message,
910 'attachments' => $attachments,
913 if (!CRM_Utils_Mail
::send($mailParams)) {
917 // add activity target record for every mail that is send
918 $activityTargetParams = [
919 'activity_id' => $activityID,
920 'contact_id' => $toID,
921 'record_type_id' => $targetID,
923 CRM_Activity_BAO_ActivityContact
::create($activityTargetParams);
928 * Get the url to redirect the user's browser to.
931 * @throws \CRM_Core_Exception
933 protected function getRedirectUrl(): string {
934 // also fix the user context stack
935 if ($this->getCaseID()) {
936 $ccid = CRM_Core_DAO
::getFieldValue('CRM_Case_DAO_CaseContact', $this->_caseId
,
937 'contact_id', 'case_id'
939 $url = CRM_Utils_System
::url('civicrm/contact/view/case',
940 "&reset=1&action=view&cid={$ccid}&id=" . $this->getCaseID()
943 elseif ($this->_context
) {
944 $url = CRM_Utils_System
::url('civicrm/dashboard', 'reset=1');
947 $url = CRM_Utils_System
::url('civicrm/contact/view',
948 "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity"
955 * Get the result rows to email.
959 * @throws \API_Exception
960 * @throws \CRM_Core_Exception
962 protected function getRowsForEmails(): array {
964 foreach ($this->getRows() as $row) {
965 $rows[$row['contact_id']][] = $row;
967 // format contact details array to handle multiple emails from same contact
968 $formattedContactDetails = [];
969 foreach ($this->getEmails() as $details) {
970 $contactID = $details['contact_id'];
971 $index = $contactID . '::' . $details['email'];
972 if (!isset($rows[$contactID])) {
973 $formattedContactDetails[$index] = $details;
976 if ($this->isGroupByContact()) {
977 foreach ($rows[$contactID] as $rowDetail) {
978 $details['schema'] = $rowDetail['schema'] ??
[];
980 $formattedContactDetails[$index] = $details;
983 foreach ($rows[$contactID] as $key => $rowDetail) {
984 $index .= '_' . $key;
985 $formattedContactDetails[$index] = $details;
986 $formattedContactDetails[$index]['schema'] = $rowDetail['schema'] ??
[];
991 return $formattedContactDetails;
995 * Only send one email per contact.
997 * This has historically been done for contributions & makes sense if
998 * no entity specific tokens are in use.
1002 protected function isGroupByContact(): bool {
1007 * Get the tokens in the submitted message.
1010 * @throws \CRM_Core_Exception
1012 protected function getMessageTokens(): array {
1013 return CRM_Utils_Token
::getTokens($this->getSubject() . $this->getSubmittedValue('html_message') . $this->getSubmittedValue('text_message'));