Add token support for participant & membership tasks
[civicrm-core.git] / CRM / Contact / Form / Task / EmailTrait.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 use Civi\Api4\Email;
19
20 /**
21 * This class provides the common functionality for tasks that send emails.
22 */
23 trait CRM_Contact_Form_Task_EmailTrait {
24
25 /**
26 * Are we operating in "single mode", i.e. sending email to one
27 * specific contact?
28 *
29 * @var bool
30 */
31 public $_single = FALSE;
32
33 /**
34 * All the existing templates in the system.
35 *
36 * @var array
37 */
38 public $_templates;
39
40 /**
41 * Email addresses to send to.
42 *
43 * @var array
44 */
45 protected $emails = [];
46
47 /**
48 * Store "to" contact details.
49 * @var array
50 */
51 public $_toContactDetails = [];
52
53 /**
54 * Store all selected contact id's, that includes to, cc and bcc contacts
55 * @var array
56 */
57 public $_allContactIds = [];
58
59 /**
60 * Store only "to" contact ids.
61 * @var array
62 */
63 public $_toContactIds = [];
64
65 /**
66 * Is the form being loaded from a search action.
67 *
68 * @var bool
69 */
70 public $isSearchContext = TRUE;
71
72 public $contactEmails = [];
73
74 /**
75 * Contacts form whom emails could not be sent.
76 *
77 * An array of contact ids and the relevant message details.
78 *
79 * @var array
80 */
81 protected $suppressedEmails = [];
82
83 /**
84 * Getter for isSearchContext.
85 *
86 * @return bool
87 */
88 public function isSearchContext(): bool {
89 return $this->isSearchContext;
90 }
91
92 /**
93 * Setter for isSearchContext.
94 *
95 * @param bool $isSearchContext
96 */
97 public function setIsSearchContext(bool $isSearchContext) {
98 $this->isSearchContext = $isSearchContext;
99 }
100
101 /**
102 * Build all the data structures needed to build the form.
103 *
104 * @throws \CiviCRM_API3_Exception
105 * @throws \CRM_Core_Exception
106 */
107 public function preProcess() {
108 $this->traitPreProcess();
109 }
110
111 /**
112 * Call trait preProcess function.
113 *
114 * This function exists as a transitional arrangement so classes overriding
115 * preProcess can still call it. Ideally it will be melded into preProcess
116 * later.
117 *
118 * @throws \CRM_Core_Exception
119 * @throws \API_Exception
120 */
121 protected function traitPreProcess() {
122 if ($this->isSearchContext()) {
123 // Currently only the contact email form is callable outside search context.
124 parent::preProcess();
125 }
126 $this->setContactIDs();
127 $this->assign('single', $this->_single);
128 if (CRM_Core_Permission::check('administer CiviCRM')) {
129 $this->assign('isAdmin', 1);
130 }
131 }
132
133 /**
134 * Build the form object.
135 *
136 * @throws \CRM_Core_Exception
137 */
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);
142
143 $toArray = [];
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');
147 if ($cid) {
148 $this->_contactIds = explode(',', $cid);
149 }
150 // The default in CRM_Core_Form_Task is null, but changing it there gives
151 // errors later.
152 if (is_null($this->_contactIds)) {
153 $this->_contactIds = [];
154 }
155 if (count($this->_contactIds) > 1) {
156 $this->_single = FALSE;
157 }
158 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
159
160 $emailAttributes = [
161 'class' => 'huge',
162 ];
163 $this->add('text', 'to', ts('To'), $emailAttributes, TRUE);
164
165 $this->addEntityRef('cc_id', ts('CC'), [
166 'entity' => 'Email',
167 'multiple' => TRUE,
168 ]);
169
170 $this->addEntityRef('bcc_id', ts('BCC'), [
171 'entity' => 'Email',
172 'multiple' => TRUE,
173 ]);
174
175 $setDefaults = TRUE;
176 if (property_exists($this, '_context') && $this->_context === 'standalone') {
177 $setDefaults = FALSE;
178 }
179
180 $this->_allContactIds = $this->_toContactIds = $this->_contactIds;
181
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;
186 }
187
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],
195 ])['values'];
196
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
201 // that we have.
202 if (!empty($this->_toEmail['contact_id']) && !empty($allContactDetails[$this->_toEmail['contact_id']])) {
203 $allContactDetails[$this->_toEmail['contact_id']]['email'] = $this->_toEmail['email'];
204 }
205
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);
210 }
211 elseif (in_array($contactId, $this->_toContactIds)) {
212 $this->_toContactDetails[$contactId] = $this->_contactDetails[$contactId] = $value;
213 $toArray[] = [
214 'text' => '"' . $value['sort_name'] . '" <' . $value['email'] . '>',
215 'id' => "$contactId::{$value['email']}",
216 ];
217 }
218 }
219
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.'));
222 }
223 }
224
225 $this->assign('toContact', json_encode($toArray));
226
227 $this->assign('suppressedEmails', count($this->suppressedEmails));
228
229 $this->assign('totalSelectedContacts', count($this->_contactIds));
230
231 $this->add('text', 'subject', ts('Subject'), ['size' => 50, 'maxlength' => 254], TRUE);
232
233 $this->add('select', 'from_email_address', ts('From'), $this->getFromEmails(), TRUE);
234
235 CRM_Mailing_BAO_Mailing::commonCompose($this);
236
237 // add attachments
238 CRM_Core_BAO_File::buildAttachment($this, NULL);
239
240 if ($this->_single) {
241 CRM_Core_Session::singleton()->replaceUserContext($this->getRedirectUrl());
242 }
243 $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
244
245 $fields = [
246 'followup_assignee_contact_id' => [
247 'type' => 'entityRef',
248 'label' => ts('Assigned to'),
249 'attributes' => [
250 'multiple' => TRUE,
251 'create' => TRUE,
252 'api' => ['params' => ['is_deceased' => 0]],
253 ],
254 ],
255 'followup_activity_type_id' => [
256 'type' => 'select',
257 'label' => ts('Followup Activity'),
258 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant::ActivityType(FALSE),
259 'extra' => ['class' => 'crm-select2'],
260 ],
261 'followup_activity_subject' => [
262 'type' => 'text',
263 'label' => ts('Subject'),
264 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity',
265 'subject'
266 ),
267 ],
268 ];
269
270 //add followup date
271 $this->add('datepicker', 'followup_date', ts('in'));
272
273 foreach ($fields as $field => $values) {
274 if (!empty($fields[$field])) {
275 $attribute = $values['attributes'] ?? NULL;
276 $required = !empty($values['required']);
277
278 if ($values['type'] === 'select' && empty($attribute)) {
279 $this->addSelect($field, ['entity' => 'activity'], $required);
280 }
281 elseif ($values['type'] === 'entityRef') {
282 $this->addEntityRef($field, $values['label'], $attribute, $required);
283 }
284 else {
285 $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values));
286 }
287 }
288 }
289
290 //Added for CRM-15984: Add campaign field
291 CRM_Campaign_BAO_Campaign::addCampaign($this);
292
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');
296 }
297
298 /**
299 * Set relevant default values.
300 *
301 * @return array
302 *
303 * @throws \API_Exception
304 * @throws \CRM_Core_Exception
305 */
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);
312 }
313 if (!Civi::settings()->get('allow_mail_from_logged_in_contact')) {
314 $defaults['from_email_address'] = current(CRM_Core_BAO_Domain::getNameAndEmail(FALSE, TRUE));
315 }
316 return $defaults;
317 }
318
319 /**
320 * Process the form after the input has been submitted and validated.
321 *
322 * @throws \CRM_Core_Exception
323 * @throws \CiviCRM_API3_Exception
324 * @throws \Civi\API\Exception\UnauthorizedException
325 * @throws \API_Exception
326 */
327 public function postProcess() {
328 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
329 $formValues = $this->controller->exportValues($this->getName());
330 $this->submit($formValues);
331 }
332
333 /**
334 * Bounce if there are more emails than permitted.
335 *
336 * @param int $count
337 * The number of emails the user is attempting to send
338 */
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.',
343 [1 => $limit]
344 ));
345 }
346 }
347
348 /**
349 * Submit the form values.
350 *
351 * This is also accessible for testing.
352 *
353 * @param array $formValues
354 *
355 * @throws \CRM_Core_Exception
356 * @throws \CiviCRM_API3_Exception
357 * @throws \Civi\API\Exception\UnauthorizedException
358 * @throws \API_Exception
359 */
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);
367
368 $ccArray = $formValues['cc_id'] ? explode(',', $formValues['cc_id']) : [];
369 $cc = $this->getEmailString($ccArray);
370 $additionalDetails = empty($ccArray) ? '' : "\ncc : " . $this->getEmailUrlString($ccArray);
371
372 $bccArray = $formValues['bcc_id'] ? explode(',', $formValues['bcc_id']) : [];
373 $bcc = $this->getEmailString($bccArray);
374 $additionalDetails .= empty($bccArray) ? '' : "\nbcc : " . $this->getEmailUrlString($bccArray);
375
376 // send the mail
377 [$sent, $activityIds] = $this->sendEmail(
378 $formValues['text_message'],
379 $formValues['html_message'],
380 $from,
381 $this->getAttachments($formValues),
382 $cc,
383 $bcc,
384 $additionalDetails,
385 CRM_Utils_Array::value('campaign_id', $formValues),
386 $this->getCaseID()
387 );
388
389 if ($sent) {
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');
401 }
402
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',
408 ]), 'info');
409 }
410 }
411
412 /**
413 * Save the template if update selected.
414 *
415 * @param array $formValues
416 *
417 * @throws \CiviCRM_API3_Exception
418 * @throws \Civi\API\Exception\UnauthorizedException
419 */
420 protected function saveMessageTemplate($formValues) {
421 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
422 $messageTemplate = [
423 'msg_text' => $formValues['text_message'],
424 'msg_html' => $formValues['html_message'],
425 'msg_subject' => $formValues['subject'],
426 'is_active' => TRUE,
427 ];
428
429 if (!empty($formValues['saveTemplate'])) {
430 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
431 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
432 }
433
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);
438 }
439 }
440 }
441
442 /**
443 * Get the emails from the added element.
444 *
445 * @return array
446 */
447 protected function getEmails(): array {
448 $allEmails = explode(',', $this->getSubmittedValue('to'));
449 $return = [];
450 foreach ($allEmails as $value) {
451 $values = explode('::', $value);
452 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];
453 }
454 return $return;
455 }
456
457 /**
458 * Get the string for the email IDs.
459 *
460 * @param array $emailIDs
461 * Array of email IDs.
462 *
463 * @return string
464 * e.g. "Smith, Bob<bob.smith@example.com>".
465 *
466 * @throws \API_Exception
467 * @throws \Civi\API\Exception\UnauthorizedException
468 */
469 protected function getEmailString(array $emailIDs): string {
470 if (empty($emailIDs)) {
471 return '';
472 }
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();
477 $emailStrings = [];
478 foreach ($emails as $email) {
479 $this->contactEmails[$email['id']] = $email;
480 $emailStrings[] = '"' . $email['contact_id.sort_name'] . '" <' . $email['email'] . '>';
481 }
482 return implode(',', $emailStrings);
483 }
484
485 /**
486 * Get the url string.
487 *
488 * This is called after the contacts have been retrieved so we don't need to re-retrieve.
489 *
490 * @param array $emailIDs
491 *
492 * @return string
493 * e.g. <a href='{$contactURL}'>Bob Smith</a>'
494 */
495 protected function getEmailUrlString(array $emailIDs): string {
496 $urls = [];
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>';
500 }
501 return implode(', ', $urls);
502 }
503
504 /**
505 * Set the emails that are not to be sent out.
506 *
507 * @param int $contactID
508 * @param array $values
509 */
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') . ')' : '');
513 }
514
515 /**
516 * Get any attachments.
517 *
518 * @param array $formValues
519 *
520 * @return array
521 */
522 protected function getAttachments(array $formValues): array {
523 $attachments = [];
524 CRM_Core_BAO_File::formatAttachment($formValues,
525 $attachments,
526 NULL, NULL
527 );
528 return $attachments;
529 }
530
531 /**
532 * Get the subject for the message.
533 *
534 * @return string
535 */
536 protected function getSubject():string {
537 return (string) $this->getSubmittedValue('subject');
538 }
539
540 /**
541 * Create any follow up activities.
542 *
543 * @param array $formValues
544 * @param int $activityId
545 *
546 * @return string
547 *
548 * @throws \CRM_Core_Exception
549 */
550 protected function createFollowUpActivities($formValues, $activityId): string {
551 $params = [];
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.');
562
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;
570 }
571
572 $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts);
573 if ($sentFollowup) {
574 $followupStatus .= '<br />' . ts('A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).');
575 }
576 }
577 }
578 }
579 return $followupStatus;
580 }
581
582 /**
583 * Form rule.
584 *
585 * @param array $fields
586 * The input form values.
587 *
588 * @return bool|array
589 * true if no errors, else array of errors
590 */
591 public static function saveTemplateFormRule(array $fields) {
592 $errors = [];
593 //Added for CRM-1393
594 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
595 $errors['saveTemplateName'] = ts('Enter name to save message template');
596 }
597 return empty($errors) ? TRUE : $errors;
598 }
599
600 /**
601 * Prevent submission of deprecated tokens.
602 *
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.
607 *
608 * @param array $fields
609 *
610 * @return bool|string[]
611 */
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}',
624 ];
625 $tokenErrors = [];
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]);
629 }
630 }
631 return empty($tokenErrors) ? TRUE : ['html_message' => implode('<br>', $tokenErrors)];
632 }
633
634 /**
635 * Get selected contribution IDs.
636 *
637 * @return array
638 */
639 protected function getContributionIDs(): array {
640 return [];
641 }
642
643 /**
644 * Get case ID - if any.
645 *
646 * @return int|null
647 *
648 * @throws \CRM_Core_Exception
649 */
650 protected function getCaseID(): ?int {
651 $caseID = CRM_Utils_Request::retrieve('caseid', 'String', $this);
652 if ($caseID) {
653 return (int) $caseID;
654 }
655 return NULL;
656 }
657
658 /**
659 * @return array
660 */
661 protected function getFromEmails(): array {
662 $fromEmailValues = CRM_Core_BAO_Email::getFromEmail();
663
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.'));
666 }
667 return $fromEmailValues;
668 }
669
670 /**
671 * Get the relevant emails.
672 *
673 * @param int $index
674 *
675 * @return string
676 */
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];
683 }
684 }
685 return $this->emails[$index];
686 }
687
688 /**
689 * Send the message to all the contacts.
690 *
691 * Do not use this function outside of core tested code. It will change.
692 *
693 * It will also become protected once tests no longer call it.
694 *
695 * @internal
696 *
697 * Also insert a contact activity in each contacts record.
698 *
699 * @param $text
700 * @param $html
701 * @param string $from
702 * @param array|null $attachments
703 * The array of attachments if any.
704 * @param string|null $cc
705 * Cc recipient.
706 * @param string|null $bcc
707 * Bcc recipient.
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
712 *
713 * @return array
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.
716 *
717 * @throws \API_Exception
718 * @throws \CRM_Core_Exception
719 * @throws \PEAR_Exception
720 * @internal
721 *
722 * Also insert a contact activity in each contacts record.
723 *
724 * @internal
725 *
726 * Also insert a contact activity in each contacts record.
727 */
728 public function sendEmail(
729 $text,
730 $html,
731 $from,
732 $attachments = NULL,
733 $cc = NULL,
734 $bcc = NULL,
735 $additionalDetails = NULL,
736 $campaignId = NULL,
737 $caseId = NULL
738 ) {
739
740 $userID = CRM_Core_Session::getLoggedInContactID();
741
742 $sent = $notSent = [];
743 $attachmentFileIds = [];
744 $activityIds = [];
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' => [
751 'msg_text' => $text,
752 'msg_html' => $html,
753 'msg_subject' => $this->getSubject(),
754 ],
755 'tokenContext' => array_merge(['schema' => $this->getTokenSchema()], ($values['schema'] ?? [])),
756 'contactId' => $contactId,
757 'disableSmarty' => !CRM_Utils_Constant::value('CIVICRM_MAIL_SMARTY'),
758 ]);
759
760 $sent = FALSE;
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);
765 }
766
767 // Create email activity.
768 $activityID = $this->createEmailActivity($userID, $renderedTemplate['subject'], $renderedTemplate['html'], $renderedTemplate['text'], $additionalDetails, $campaignId, $attachments, $caseId);
769 $activityIds[] = $activityID;
770
771 if ($firstActivityCreated == FALSE && !empty($attachments)) {
772 $attachmentFileIds = CRM_Activity_BAO_Activity::getAttachmentFileIds($activityID, $attachments);
773 $firstActivityCreated = TRUE;
774 }
775
776 if ($this->sendMessage(
777 $from,
778 $contactId,
779 $renderedTemplate['subject'],
780 $renderedTemplate['text'],
781 $renderedTemplate['html'],
782 $emailAddress,
783 $activityID,
784 // get the set of attachments from where they are stored
785 CRM_Core_BAO_File::getEntityFile('civicrm_activity', $activityID),
786 $cc,
787 $bcc
788 )
789 ) {
790 $sent = TRUE;
791 }
792 }
793
794 return [$sent, $activityIds];
795 }
796
797 /**
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
807 * @param int $caseID
808 *
809 * @return int
810 * The created activity ID
811 * @throws \CRM_Core_Exception
812 */
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');
815
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";
819 }
820 else {
821 $details = $html ? $html : $text;
822 $details .= $additionalDetails;
823 }
824
825 $activityParams = [
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,
833 ];
834 if (!empty($caseID)) {
835 $activityParams['case_id'] = $caseID;
836 }
837
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']);
840
841 // add the attachments to activity params here
842 if ($attachments) {
843 // first process them
844 $activityParams = array_merge($activityParams, $attachments);
845 }
846
847 $activity = civicrm_api3('Activity', 'create', $activityParams);
848
849 return $activity['id'];
850 }
851
852 /**
853 * Send message - under refactor.
854 *
855 * @param $from
856 * @param $toID
857 * @param $subject
858 * @param $text_message
859 * @param $html_message
860 * @param $emailAddress
861 * @param $activityID
862 * @param null $attachments
863 * @param null $cc
864 * @param null $bcc
865 *
866 * @return bool
867 * @throws \CRM_Core_Exception
868 * @throws \PEAR_Exception
869 */
870 protected function sendMessage(
871 $from,
872 $toID,
873 $subject,
874 $text_message,
875 $html_message,
876 $emailAddress,
877 $activityID,
878 $attachments = NULL,
879 $cc = NULL,
880 $bcc = NULL
881 ) {
882 [$toDisplayName, $toEmail, $toDoNotEmail] = CRM_Contact_BAO_Contact::getContactDetails($toID);
883 if ($emailAddress) {
884 $toEmail = trim($emailAddress);
885 }
886
887 // make sure both email addresses are valid
888 // and that the recipient wants to receive email
889 if (empty($toEmail) or $toDoNotEmail) {
890 return FALSE;
891 }
892 if (!trim($toDisplayName)) {
893 $toDisplayName = $toEmail;
894 }
895
896 $activityContacts = CRM_Activity_BAO_ActivityContact::buildOptions('record_type_id', 'validate');
897 $targetID = CRM_Utils_Array::key('Activity Targets', $activityContacts);
898
899 // create the params array
900 $mailParams = [
901 'groupName' => 'Activity Email Sender',
902 'from' => $from,
903 'toName' => $toDisplayName,
904 'toEmail' => $toEmail,
905 'subject' => $subject,
906 'cc' => $cc,
907 'bcc' => $bcc,
908 'text' => $text_message,
909 'html' => $html_message,
910 'attachments' => $attachments,
911 ];
912
913 if (!CRM_Utils_Mail::send($mailParams)) {
914 return FALSE;
915 }
916
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,
922 ];
923 CRM_Activity_BAO_ActivityContact::create($activityTargetParams);
924 return TRUE;
925 }
926
927 /**
928 * Get the url to redirect the user's browser to.
929 *
930 * @return string
931 * @throws \CRM_Core_Exception
932 */
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'
938 );
939 $url = CRM_Utils_System::url('civicrm/contact/view/case',
940 "&reset=1&action=view&cid={$ccid}&id=" . $this->getCaseID()
941 );
942 }
943 elseif ($this->_context) {
944 $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1');
945 }
946 else {
947 $url = CRM_Utils_System::url('civicrm/contact/view',
948 "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity"
949 );
950 }
951 return $url;
952 }
953
954 /**
955 * Get the result rows to email.
956 *
957 * @return array
958 *
959 * @throws \API_Exception
960 * @throws \CRM_Core_Exception
961 */
962 protected function getRowsForEmails(): array {
963 $rows = [];
964 foreach ($this->getRows() as $row) {
965 $rows[$row['contact_id']][] = $row;
966 }
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;
974 continue;
975 }
976 if ($this->isGroupByContact()) {
977 foreach ($rows[$contactID] as $rowDetail) {
978 $details['schema'] = $rowDetail['schema'] ?? [];
979 }
980 $formattedContactDetails[$index] = $details;
981 }
982 else {
983 foreach ($rows[$contactID] as $key => $rowDetail) {
984 $index .= '_' . $key;
985 $formattedContactDetails[$index] = $details;
986 $formattedContactDetails[$index]['schema'] = $rowDetail['schema'] ?? [];
987 }
988 }
989
990 }
991 return $formattedContactDetails;
992 }
993
994 /**
995 * Only send one email per contact.
996 *
997 * This has historically been done for contributions & makes sense if
998 * no entity specific tokens are in use.
999 *
1000 * @return bool
1001 */
1002 protected function isGroupByContact(): bool {
1003 return TRUE;
1004 }
1005
1006 /**
1007 * Get the tokens in the submitted message.
1008 *
1009 * @return array
1010 * @throws \CRM_Core_Exception
1011 */
1012 protected function getMessageTokens(): array {
1013 return CRM_Utils_Token::getTokens($this->getSubject() . $this->getSubmittedValue('html_message') . $this->getSubmittedValue('text_message'));
1014 }
1015
1016 }