Return the sendEmail function to it's owner
[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 use Civi\Api4\Contribution;
20
21 /**
22 * This class provides the common functionality for tasks that send emails.
23 */
24 trait CRM_Contact_Form_Task_EmailTrait {
25
26 /**
27 * Are we operating in "single mode", i.e. sending email to one
28 * specific contact?
29 *
30 * @var bool
31 */
32 public $_single = FALSE;
33
34 /**
35 * All the existing templates in the system.
36 *
37 * @var array
38 */
39 public $_templates;
40
41 /**
42 * Email addresses to send to.
43 *
44 * @var array
45 */
46 protected $emails = [];
47
48 /**
49 * Store "to" contact details.
50 * @var array
51 */
52 public $_toContactDetails = [];
53
54 /**
55 * Store all selected contact id's, that includes to, cc and bcc contacts
56 * @var array
57 */
58 public $_allContactIds = [];
59
60 /**
61 * Store only "to" contact ids.
62 * @var array
63 */
64 public $_toContactIds = [];
65
66 /**
67 * Is the form being loaded from a search action.
68 *
69 * @var bool
70 */
71 public $isSearchContext = TRUE;
72
73 public $contactEmails = [];
74
75 /**
76 * Contacts form whom emails could not be sent.
77 *
78 * An array of contact ids and the relevant message details.
79 *
80 * @var array
81 */
82 protected $suppressedEmails = [];
83
84 /**
85 * Getter for isSearchContext.
86 *
87 * @return bool
88 */
89 public function isSearchContext(): bool {
90 return $this->isSearchContext;
91 }
92
93 /**
94 * Setter for isSearchContext.
95 *
96 * @param bool $isSearchContext
97 */
98 public function setIsSearchContext(bool $isSearchContext) {
99 $this->isSearchContext = $isSearchContext;
100 }
101
102 /**
103 * Build all the data structures needed to build the form.
104 *
105 * @throws \CiviCRM_API3_Exception
106 * @throws \CRM_Core_Exception
107 */
108 public function preProcess() {
109 $this->traitPreProcess();
110 }
111
112 /**
113 * Call trait preProcess function.
114 *
115 * This function exists as a transitional arrangement so classes overriding
116 * preProcess can still call it. Ideally it will be melded into preProcess
117 * later.
118 *
119 * @throws \CRM_Core_Exception
120 * @throws \API_Exception
121 */
122 protected function traitPreProcess() {
123 $this->preProcessFromAddress();
124 if ($this->isSearchContext()) {
125 // Currently only the contact email form is callable outside search context.
126 parent::preProcess();
127 }
128 $this->setContactIDs();
129 $this->assign('single', $this->_single);
130 if (CRM_Core_Permission::check('administer CiviCRM')) {
131 $this->assign('isAdmin', 1);
132 }
133 }
134
135 /**
136 * Pre Process Form Addresses to be used in Quickform
137 *
138 * @throws \API_Exception
139 * @throws \CRM_Core_Exception
140 */
141 protected function preProcessFromAddress(): void {
142 $form = $this;
143 $form->_emails = [];
144
145 // @TODO remove these line and to it somewhere more appropriate. Currently some classes (e.g Case
146 // are having to re-write contactIds afterwards due to this inappropriate variable setting
147 // If we don't have any contact IDs, use the logged in contact ID
148 $form->_contactIds = $form->_contactIds ?: [CRM_Core_Session::getLoggedInContactID()];
149 }
150
151 /**
152 * Build the form object.
153 *
154 * @throws \CRM_Core_Exception
155 */
156 public function buildQuickForm() {
157 // Suppress form might not be required but perhaps there was a risk some other process had set it to TRUE.
158 $this->assign('suppressForm', FALSE);
159 $this->assign('emailTask', TRUE);
160
161 $toArray = [];
162 $suppressedEmails = 0;
163 //here we are getting logged in user id as array but we need target contact id. CRM-5988
164 $cid = $this->get('cid');
165 if ($cid) {
166 $this->_contactIds = explode(',', $cid);
167 }
168 if (count($this->_contactIds) > 1) {
169 $this->_single = FALSE;
170 }
171 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
172
173 $emailAttributes = [
174 'class' => 'huge',
175 ];
176 $to = $this->add('text', 'to', ts('To'), $emailAttributes, TRUE);
177
178 $this->addEntityRef('cc_id', ts('CC'), [
179 'entity' => 'Email',
180 'multiple' => TRUE,
181 ]);
182
183 $this->addEntityRef('bcc_id', ts('BCC'), [
184 'entity' => 'Email',
185 'multiple' => TRUE,
186 ]);
187
188 if ($to->getValue()) {
189 $this->_toContactIds = $this->_contactIds = [];
190 }
191 $setDefaults = TRUE;
192 if (property_exists($this, '_context') && $this->_context === 'standalone') {
193 $setDefaults = FALSE;
194 }
195
196 $this->_allContactIds = $this->_toContactIds = $this->_contactIds;
197
198 if ($to->getValue()) {
199 foreach ($this->getEmails($to) as $value) {
200 $contactId = $value['contact_id'];
201 if ($contactId) {
202 $this->_contactIds[] = $this->_toContactIds[] = $contactId;
203 $this->_allContactIds[] = $contactId;
204 }
205 }
206 $setDefaults = TRUE;
207 }
208
209 //get the group of contacts as per selected by user in case of Find Activities
210 if (!empty($this->_activityHolderIds)) {
211 $contact = $this->get('contacts');
212 $this->_allContactIds = $this->_contactIds = $contact;
213 }
214
215 // check if we need to setdefaults and check for valid contact emails / communication preferences
216 if (is_array($this->_allContactIds) && $setDefaults) {
217 // get the details for all selected contacts ( to, cc and bcc contacts )
218 $allContactDetails = civicrm_api3('Contact', 'get', [
219 'id' => ['IN' => $this->_allContactIds],
220 'return' => ['sort_name', 'email', 'do_not_email', 'is_deceased', 'on_hold', 'display_name', 'preferred_mail_format'],
221 'options' => ['limit' => 0],
222 ])['values'];
223
224 // The contact task supports passing in email_id in a url. It supports a single email
225 // and is marked as having been related to CiviHR.
226 // The array will look like $this->_toEmail = ['email' => 'x', 'contact_id' => 2])
227 // If it exists we want to use the specified email which might be different to the primary email
228 // that we have.
229 if (!empty($this->_toEmail['contact_id']) && !empty($allContactDetails[$this->_toEmail['contact_id']])) {
230 $allContactDetails[$this->_toEmail['contact_id']]['email'] = $this->_toEmail['email'];
231 }
232
233 // perform all validations on unique contact Ids
234 foreach ($allContactDetails as $contactId => $value) {
235 if ($value['do_not_email'] || empty($value['email']) || !empty($value['is_deceased']) || $value['on_hold']) {
236 $this->setSuppressedEmail($contactId, $value);
237 }
238 elseif (in_array($contactId, $this->_toContactIds)) {
239 $this->_toContactDetails[$contactId] = $this->_contactDetails[$contactId] = $value;
240 $toArray[] = [
241 'text' => '"' . $value['sort_name'] . '" <' . $value['email'] . '>',
242 'id' => "$contactId::{$value['email']}",
243 ];
244 }
245 }
246
247 if (empty($toArray)) {
248 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.'));
249 }
250 }
251
252 $this->assign('toContact', json_encode($toArray));
253
254 $this->assign('suppressedEmails', count($this->suppressedEmails));
255
256 $this->assign('totalSelectedContacts', count($this->_contactIds));
257
258 $this->add('text', 'subject', ts('Subject'), ['size' => 50, 'maxlength' => 254], TRUE);
259
260 $this->add('select', 'from_email_address', ts('From'), $this->getFromEmails(), TRUE);
261
262 CRM_Mailing_BAO_Mailing::commonCompose($this);
263
264 // add attachments
265 CRM_Core_BAO_File::buildAttachment($this, NULL);
266
267 if ($this->_single) {
268 // also fix the user context stack
269 if ($this->getCaseID()) {
270 $ccid = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $this->_caseId,
271 'contact_id', 'case_id'
272 );
273 $url = CRM_Utils_System::url('civicrm/contact/view/case',
274 "&reset=1&action=view&cid={$ccid}&id=" . $this->getCaseID()
275 );
276 }
277 elseif ($this->_context) {
278 $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1');
279 }
280 else {
281 $url = CRM_Utils_System::url('civicrm/contact/view',
282 "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity"
283 );
284 }
285
286 $session = CRM_Core_Session::singleton();
287 $session->replaceUserContext($url);
288 }
289 $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
290
291 $fields = [
292 'followup_assignee_contact_id' => [
293 'type' => 'entityRef',
294 'label' => ts('Assigned to'),
295 'attributes' => [
296 'multiple' => TRUE,
297 'create' => TRUE,
298 'api' => ['params' => ['is_deceased' => 0]],
299 ],
300 ],
301 'followup_activity_type_id' => [
302 'type' => 'select',
303 'label' => ts('Followup Activity'),
304 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant::ActivityType(FALSE),
305 'extra' => ['class' => 'crm-select2'],
306 ],
307 'followup_activity_subject' => [
308 'type' => 'text',
309 'label' => ts('Subject'),
310 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity',
311 'subject'
312 ),
313 ],
314 ];
315
316 //add followup date
317 $this->add('datepicker', 'followup_date', ts('in'));
318
319 foreach ($fields as $field => $values) {
320 if (!empty($fields[$field])) {
321 $attribute = $values['attributes'] ?? NULL;
322 $required = !empty($values['required']);
323
324 if ($values['type'] === 'select' && empty($attribute)) {
325 $this->addSelect($field, ['entity' => 'activity'], $required);
326 }
327 elseif ($values['type'] === 'entityRef') {
328 $this->addEntityRef($field, $values['label'], $attribute, $required);
329 }
330 else {
331 $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values));
332 }
333 }
334 }
335
336 //Added for CRM-15984: Add campaign field
337 CRM_Campaign_BAO_Campaign::addCampaign($this);
338
339 $this->addFormRule([__CLASS__, 'saveTemplateFormRule'], $this);
340 $this->addFormRule([__CLASS__, 'deprecatedTokensFormRule'], $this);
341 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
342 }
343
344 /**
345 * Set relevant default values.
346 *
347 * @return array
348 *
349 * @throws \API_Exception
350 * @throws \CRM_Core_Exception
351 */
352 public function setDefaultValues(): array {
353 $defaults = parent::setDefaultValues();
354 $fromEmails = $this->getFromEmails();
355 if (is_numeric(key($fromEmails))) {
356 $emailID = (int) key($fromEmails);
357 $defaults = CRM_Core_BAO_Email::getEmailSignatureDefaults($emailID);
358 }
359 if (!Civi::settings()->get('allow_mail_from_logged_in_contact')) {
360 $defaults['from_email_address'] = current(CRM_Core_BAO_Domain::getNameAndEmail(FALSE, TRUE));
361 }
362 return $defaults;
363 }
364
365 /**
366 * Process the form after the input has been submitted and validated.
367 *
368 * @throws \CRM_Core_Exception
369 * @throws \CiviCRM_API3_Exception
370 * @throws \Civi\API\Exception\UnauthorizedException
371 * @throws \API_Exception
372 */
373 public function postProcess() {
374 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
375 $formValues = $this->controller->exportValues($this->getName());
376 $this->submit($formValues);
377 }
378
379 /**
380 * Bounce if there are more emails than permitted.
381 *
382 * @param int $count
383 * The number of emails the user is attempting to send
384 */
385 protected function bounceIfSimpleMailLimitExceeded($count): void {
386 $limit = Civi::settings()->get('simple_mail_limit');
387 if ($count > $limit) {
388 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.',
389 [1 => $limit]
390 ));
391 }
392 }
393
394 /**
395 * Submit the form values.
396 *
397 * This is also accessible for testing.
398 *
399 * @param array $formValues
400 *
401 * @throws \CRM_Core_Exception
402 * @throws \CiviCRM_API3_Exception
403 * @throws \Civi\API\Exception\UnauthorizedException
404 * @throws \API_Exception
405 */
406 public function submit($formValues): void {
407 $this->saveMessageTemplate($formValues);
408
409 $from = $formValues['from_email_address'] ?? NULL;
410 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
411 // If we have had a contact email used here the value returned from the line above will be the
412 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
413 $from = CRM_Utils_Mail::formatFromAddress($from);
414
415 $ccArray = $formValues['cc_id'] ? explode(',', $formValues['cc_id']) : [];
416 $cc = $this->getEmailString($ccArray);
417 $additionalDetails = empty($ccArray) ? '' : "\ncc : " . $this->getEmailUrlString($ccArray);
418
419 $bccArray = $formValues['bcc_id'] ? explode(',', $formValues['bcc_id']) : [];
420 $bcc = $this->getEmailString($bccArray);
421 $additionalDetails .= empty($bccArray) ? '' : "\nbcc : " . $this->getEmailUrlString($bccArray);
422
423 // format contact details array to handle multiple emails from same contact
424 $formattedContactDetails = [];
425 foreach ($this->_contactIds as $key => $contactId) {
426 // if we dont have details on this contactID, we should ignore
427 // potentially this is due to the contact not wanting to receive email
428 if (!isset($this->_contactDetails[$contactId])) {
429 continue;
430 }
431 $email = $this->getEmail($key);
432 // prevent duplicate emails if same email address is selected CRM-4067
433 // we should allow same emails for different contacts
434 $details = $this->_contactDetails[$contactId];
435 $details['email'] = $email;
436 unset($details['email_id']);
437 $formattedContactDetails["{$contactId}::{$email}"] = $details;
438 }
439
440 // send the mail
441 [$sent, $activityIds] = $this->sendEmail(
442 $formattedContactDetails,
443 $this->getSubject($formValues['subject']),
444 $formValues['text_message'],
445 $formValues['html_message'],
446 NULL,
447 NULL,
448 $from,
449 $this->getAttachments($formValues),
450 $cc,
451 $bcc,
452 array_keys($this->_toContactDetails),
453 $additionalDetails,
454 $this->getContributionIDs(),
455 CRM_Utils_Array::value('campaign_id', $formValues),
456 $this->getCaseID()
457 );
458
459 if ($sent) {
460 // Only use the first activity id if there's multiple.
461 // If there's multiple recipients the idea behind multiple activities
462 // is to record the token value replacements separately, but that
463 // has no meaning for followup activities, and this doesn't prevent
464 // creating more manually if desired.
465 $followupStatus = $this->createFollowUpActivities($formValues, $activityIds[0]);
466 $count_success = count($this->_toContactDetails);
467 CRM_Core_Session::setStatus(ts('One message was sent successfully. ', [
468 'plural' => '%count messages were sent successfully. ',
469 'count' => $count_success,
470 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
471 }
472
473 if (!empty($this->suppressedEmails)) {
474 $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>';
475 CRM_Core_Session::setStatus($status, ts('One Message Not Sent', [
476 'count' => count($this->suppressedEmails),
477 'plural' => '%count Messages Not Sent',
478 ]), 'info');
479 }
480 }
481
482 /**
483 * Save the template if update selected.
484 *
485 * @param array $formValues
486 *
487 * @throws \CiviCRM_API3_Exception
488 * @throws \Civi\API\Exception\UnauthorizedException
489 */
490 protected function saveMessageTemplate($formValues) {
491 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
492 $messageTemplate = [
493 'msg_text' => $formValues['text_message'],
494 'msg_html' => $formValues['html_message'],
495 'msg_subject' => $formValues['subject'],
496 'is_active' => TRUE,
497 ];
498
499 if (!empty($formValues['saveTemplate'])) {
500 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
501 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
502 }
503
504 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
505 $messageTemplate['id'] = $formValues['template'];
506 unset($messageTemplate['msg_title']);
507 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
508 }
509 }
510 }
511
512 /**
513 * List available tokens for this form.
514 *
515 * @return array
516 */
517 public function listTokens() {
518 return CRM_Core_SelectValues::contactTokens();
519 }
520
521 /**
522 * Get the emails from the added element.
523 *
524 * @param HTML_QuickForm_Element $element
525 *
526 * @return array
527 */
528 protected function getEmails($element): array {
529 $allEmails = explode(',', $element->getValue());
530 $return = [];
531 foreach ($allEmails as $value) {
532 $values = explode('::', $value);
533 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];
534 }
535 return $return;
536 }
537
538 /**
539 * Get the string for the email IDs.
540 *
541 * @param array $emailIDs
542 * Array of email IDs.
543 *
544 * @return string
545 * e.g. "Smith, Bob<bob.smith@example.com>".
546 *
547 * @throws \API_Exception
548 * @throws \Civi\API\Exception\UnauthorizedException
549 */
550 protected function getEmailString(array $emailIDs): string {
551 if (empty($emailIDs)) {
552 return '';
553 }
554 $emails = Email::get()
555 ->addWhere('id', 'IN', $emailIDs)
556 ->setCheckPermissions(FALSE)
557 ->setSelect(['contact_id', 'email', 'contact_id.sort_name', 'contact_id.display_name'])->execute();
558 $emailStrings = [];
559 foreach ($emails as $email) {
560 $this->contactEmails[$email['id']] = $email;
561 $emailStrings[] = '"' . $email['contact_id.sort_name'] . '" <' . $email['email'] . '>';
562 }
563 return implode(',', $emailStrings);
564 }
565
566 /**
567 * Get the url string.
568 *
569 * This is called after the contacts have been retrieved so we don't need to re-retrieve.
570 *
571 * @param array $emailIDs
572 *
573 * @return string
574 * e.g. <a href='{$contactURL}'>Bob Smith</a>'
575 */
576 protected function getEmailUrlString(array $emailIDs): string {
577 $urls = [];
578 foreach ($emailIDs as $email) {
579 $contactURL = CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'cid' => $this->contactEmails[$email]['contact_id']], TRUE);
580 $urls[] = "<a href='{$contactURL}'>" . $this->contactEmails[$email]['contact_id.display_name'] . '</a>';
581 }
582 return implode(', ', $urls);
583 }
584
585 /**
586 * Set the emails that are not to be sent out.
587 *
588 * @param int $contactID
589 * @param array $values
590 */
591 protected function setSuppressedEmail($contactID, $values) {
592 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $contactID);
593 $this->suppressedEmails[$contactID] = "<a href='$contactViewUrl' title='{$values['email']}'>{$values['display_name']}</a>" . ($values['on_hold'] ? '(' . ts('on hold') . ')' : '');
594 }
595
596 /**
597 * Get any attachments.
598 *
599 * @param array $formValues
600 *
601 * @return array
602 */
603 protected function getAttachments(array $formValues): array {
604 $attachments = [];
605 CRM_Core_BAO_File::formatAttachment($formValues,
606 $attachments,
607 NULL, NULL
608 );
609 return $attachments;
610 }
611
612 /**
613 * Get the subject for the message.
614 *
615 * The case handling should possibly be on the case form.....
616 *
617 * @param string $subject
618 *
619 * @return string
620 * @throws \CRM_Core_Exception
621 */
622 protected function getSubject(string $subject):string {
623 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
624 if ($this->getCaseID()) {
625 $hash = substr(sha1(CIVICRM_SITE_KEY . $this->getCaseID()), 0, 7);
626 $subject = "[case #$hash] $subject";
627 }
628 return $subject;
629 }
630
631 /**
632 * Create any follow up activities.
633 *
634 * @param array $formValues
635 * @param int $activityId
636 *
637 * @return string
638 *
639 * @throws \CRM_Core_Exception
640 */
641 protected function createFollowUpActivities($formValues, $activityId): string {
642 $params = [];
643 $followupStatus = '';
644 $followupActivity = NULL;
645 if (!empty($formValues['followup_activity_type_id'])) {
646 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
647 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
648 $params['followup_date'] = $formValues['followup_date'];
649 $params['target_contact_id'] = $this->_contactIds;
650 $params['followup_assignee_contact_id'] = array_filter(explode(',', $formValues['followup_assignee_contact_id']));
651 $followupActivity = CRM_Activity_BAO_Activity::createFollowupActivity($activityId, $params);
652 $followupStatus = ts('A followup activity has been scheduled.');
653
654 if (Civi::settings()->get('activity_assignee_notification')) {
655 if ($followupActivity) {
656 $mailToFollowupContacts = [];
657 $assignee = [$followupActivity->id];
658 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment::getAssigneeNames($assignee, TRUE, FALSE);
659 foreach ($assigneeContacts as $values) {
660 $mailToFollowupContacts[$values['email']] = $values;
661 }
662
663 $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts);
664 if ($sentFollowup) {
665 $followupStatus .= '<br />' . ts('A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).');
666 }
667 }
668 }
669 }
670 return $followupStatus;
671 }
672
673 /**
674 * Form rule.
675 *
676 * @param array $fields
677 * The input form values.
678 *
679 * @return bool|array
680 * true if no errors, else array of errors
681 */
682 public static function saveTemplateFormRule(array $fields) {
683 $errors = [];
684 //Added for CRM-1393
685 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
686 $errors['saveTemplateName'] = ts('Enter name to save message template');
687 }
688 return empty($errors) ? TRUE : $errors;
689 }
690
691 /**
692 * Prevent submission of deprecated tokens.
693 *
694 * Note this rule can be removed after a transition period.
695 * It's mostly to help to ensure users don't get missing tokens
696 * or unexpected output after the 5.43 upgrade until any
697 * old templates have aged out.
698 *
699 * @param array $fields
700 *
701 * @return bool|string[]
702 */
703 public static function deprecatedTokensFormRule(array $fields) {
704 $deprecatedTokens = [
705 '{case.status_id}' => '{case.status_id:label}',
706 '{case.case_type_id}' => '{case.case_type_id:label}',
707 '{contribution.campaign}' => '{contribution.campaign_id:label}',
708 '{contribution.payment_instrument}' => '{contribution.payment_instrument_id:label}',
709 '{contribution.contribution_id}' => '{contribution.id}',
710 '{contribution.contribution_source}' => '{contribution.source}',
711 '{contribution.contribution_status}' => '{contribution.contribution_status_id:label}',
712 '{contribution.contribution_cancel_date}' => '{contribution.cancel_date}',
713 '{contribution.type}' => '{contribution.financial_type_id:label}',
714 '{contribution.contribution_page_id}' => '{contribution.contribution_page_id:label}',
715 ];
716 $tokenErrors = [];
717 foreach ($deprecatedTokens as $token => $replacement) {
718 if (strpos($fields['html_message'], $token) !== FALSE) {
719 $tokenErrors[] = ts('Token %1 is no longer supported - use %2 instead', [$token, $replacement]);
720 }
721 }
722 return empty($tokenErrors) ? TRUE : ['html_message' => implode('<br>', $tokenErrors)];
723 }
724
725 /**
726 * Get selected contribution IDs.
727 *
728 * @return array
729 */
730 protected function getContributionIDs(): array {
731 return [];
732 }
733
734 /**
735 * Get case ID - if any.
736 *
737 * @return int|null
738 *
739 * @throws \CRM_Core_Exception
740 */
741 protected function getCaseID(): ?int {
742 $caseID = CRM_Utils_Request::retrieve('caseid', 'String', $this);
743 if ($caseID) {
744 return (int) $caseID;
745 }
746 return NULL;
747 }
748
749 /**
750 * @return array
751 */
752 protected function getFromEmails(): array {
753 $fromEmailValues = CRM_Core_BAO_Email::getFromEmail();
754
755 if (empty($fromEmailValues)) {
756 CRM_Core_Error::statusBounce(ts('Your user record does not have a valid email address and no from addresses have been configured.'));
757 }
758 return $fromEmailValues;
759 }
760
761 /**
762 * Get the relevant emails.
763 *
764 * @param int $index
765 *
766 * @return string
767 */
768 protected function getEmail(int $index): string {
769 if (empty($this->emails)) {
770 $toEmails = explode(',', $this->getSubmittedValue('to'));
771 foreach ($toEmails as $value) {
772 $parts = explode('::', $value);
773 $this->emails[] = $parts[1];
774 }
775 }
776 return $this->emails[$index];
777 }
778
779 /**
780 * Send the message to all the contacts.
781 *
782 * Do not use this function outside of core tested code. It will change.
783 *
784 * @internal
785 *
786 * Also insert a contact activity in each contacts record.
787 *
788 * @param array $contactDetails
789 * The array of contact details to send the email.
790 * @param string $subject
791 * The subject of the message.
792 * @param $text
793 * @param $html
794 * @param string $emailAddress
795 * Use this 'to' email address instead of the default Primary address.
796 * @param int|null $userID
797 * Use this userID if set.
798 * @param string|null $from
799 * @param array|null $attachments
800 * The array of attachments if any.
801 * @param string|null $cc
802 * Cc recipient.
803 * @param string|null $bcc
804 * Bcc recipient.
805 * @param array|null $contactIds
806 * unused.
807 * @param string|null $additionalDetails
808 * The additional information of CC and BCC appended to the activity Details.
809 * @param array|null $contributionIds
810 * @param int|null $campaignId
811 * @param int|null $caseId
812 *
813 * @return array
814 * bool $sent FIXME: this only indicates the status of the last email sent.
815 * array $activityIds The activity ids created, one per "To" recipient.
816 *
817 * @throws \API_Exception
818 * @throws \CRM_Core_Exception
819 */
820 public function sendEmail(
821 $contactDetails,
822 $subject,
823 $text,
824 $html,
825 $emailAddress,
826 $userID = NULL,
827 $from = NULL,
828 $attachments = NULL,
829 $cc = NULL,
830 $bcc = NULL,
831 $contactIds = NULL,
832 $additionalDetails = NULL,
833 $contributionIds = NULL,
834 $campaignId = NULL,
835 $caseId = NULL
836 ) {
837 // get the contact details of logged in contact, which we set as from email
838 if ($userID == NULL) {
839 $userID = CRM_Core_Session::getLoggedInContactID();
840 }
841
842 [$fromDisplayName, $fromEmail, $fromDoNotEmail] = CRM_Contact_BAO_Contact::getContactDetails($userID);
843 if (!$fromEmail) {
844 return [count($contactDetails), 0, count($contactDetails)];
845 }
846 if (!trim($fromDisplayName)) {
847 $fromDisplayName = $fromEmail;
848 }
849
850 if (!$from) {
851 $from = "$fromDisplayName <$fromEmail>";
852 }
853
854 $contributionDetails = [];
855 if (!empty($contributionIds)) {
856 $contributionDetails = Contribution::get(FALSE)
857 ->setSelect(['contact_id'])
858 ->addWhere('id', 'IN', $contributionIds)
859 ->execute()
860 // Note that this indexing means that only the last
861 // contribution per contact is resolved to tokens.
862 // this is long-standing functionality, albeit possibly
863 // not thought through.
864 ->indexBy('contact_id');
865 }
866
867 $sent = $notSent = [];
868 $attachmentFileIds = [];
869 $activityIds = [];
870 $firstActivityCreated = FALSE;
871 foreach ($contactDetails as $values) {
872 $tokenContext = $caseId ? ['caseId' => $caseId] : [];
873 $contactId = $values['contact_id'];
874 $emailAddress = $values['email'];
875
876 if (!empty($contributionDetails)) {
877 $tokenContext['contributionId'] = $contributionDetails[$contactId]['id'];
878 }
879
880 $tokenSubject = $subject;
881 $tokenText = in_array($values['preferred_mail_format'], ['Both', 'Text'], TRUE) ? $text : '';
882 $tokenHtml = in_array($values['preferred_mail_format'], ['Both', 'HTML'], TRUE) ? $html : '';
883
884 $renderedTemplate = CRM_Core_BAO_MessageTemplate::renderTemplate([
885 'messageTemplate' => [
886 'msg_text' => $tokenText,
887 'msg_html' => $tokenHtml,
888 'msg_subject' => $tokenSubject,
889 ],
890 'tokenContext' => $tokenContext,
891 'contactId' => $contactId,
892 'disableSmarty' => !CRM_Utils_Constant::value('CIVICRM_MAIL_SMARTY'),
893 ]);
894
895 $sent = FALSE;
896 // To minimize storage requirements, only one copy of any file attachments uploaded to CiviCRM is kept,
897 // even when multiple contacts will receive separate emails from CiviCRM.
898 if (!empty($attachmentFileIds)) {
899 $attachments = array_replace_recursive($attachments, $attachmentFileIds);
900 }
901
902 // Create email activity.
903 $activityID = CRM_Activity_BAO_Activity::createEmailActivity($userID, $renderedTemplate['subject'], $renderedTemplate['html'], $renderedTemplate['text'], $additionalDetails, $campaignId, $attachments, $caseId);
904 $activityIds[] = $activityID;
905
906 if ($firstActivityCreated == FALSE && !empty($attachments)) {
907 $attachmentFileIds = CRM_Activity_BAO_Activity::getAttachmentFileIds($activityID, $attachments);
908 $firstActivityCreated = TRUE;
909 }
910
911 if (CRM_Activity_BAO_Activity::sendMessage(
912 $from,
913 $userID,
914 $contactId,
915 $renderedTemplate['subject'],
916 $renderedTemplate['text'],
917 $renderedTemplate['html'],
918 $emailAddress,
919 $activityID,
920 // get the set of attachments from where they are stored
921 CRM_Core_BAO_File::getEntityFile('civicrm_activity', $activityID),
922 $cc,
923 $bcc
924 )
925 ) {
926 $sent = TRUE;
927 }
928 }
929
930 return [$sent, $activityIds];
931 }
932
933 }