[REF] Simplify subject code. Note sendEmail
[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 public $_noEmails = FALSE;
34
35 /**
36 * All the existing templates in the system.
37 *
38 * @var array
39 */
40 public $_templates;
41
42 /**
43 * Store "to" contact details.
44 * @var array
45 */
46 public $_toContactDetails = [];
47
48 /**
49 * Store all selected contact id's, that includes to, cc and bcc contacts
50 * @var array
51 */
52 public $_allContactIds = [];
53
54 /**
55 * Store only "to" contact ids.
56 * @var array
57 */
58 public $_toContactIds = [];
59
60 /**
61 * Store only "cc" contact ids.
62 * @var array
63 */
64 public $_ccContactIds = [];
65
66 /**
67 * Store only "bcc" contact ids.
68 *
69 * @var array
70 */
71 public $_bccContactIds = [];
72
73 /**
74 * Is the form being loaded from a search action.
75 *
76 * @var bool
77 */
78 public $isSearchContext = TRUE;
79
80 public $contactEmails = [];
81
82 /**
83 * Contacts form whom emails could not be sent.
84 *
85 * An array of contact ids and the relevant message details.
86 *
87 * @var array
88 */
89 protected $suppressedEmails = [];
90
91 /**
92 * Getter for isSearchContext.
93 *
94 * @return bool
95 */
96 public function isSearchContext(): bool {
97 return $this->isSearchContext;
98 }
99
100 /**
101 * Setter for isSearchContext.
102 *
103 * @param bool $isSearchContext
104 */
105 public function setIsSearchContext(bool $isSearchContext) {
106 $this->isSearchContext = $isSearchContext;
107 }
108
109 /**
110 * Build all the data structures needed to build the form.
111 *
112 * @throws \CiviCRM_API3_Exception
113 * @throws \CRM_Core_Exception
114 */
115 public function preProcess() {
116 $this->traitPreProcess();
117 }
118
119 /**
120 * Call trait preProcess function.
121 *
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.
124 *
125 * @throws \CiviCRM_API3_Exception
126 * @throws \CRM_Core_Exception
127 */
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();
133 }
134 $this->setContactIDs();
135 $this->assign('single', $this->_single);
136 if (CRM_Core_Permission::check('administer CiviCRM')) {
137 $this->assign('isAdmin', 1);
138 }
139 }
140
141 /**
142 * Build the form object.
143 *
144 * @throws \CRM_Core_Exception
145 */
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);
150
151 $toArray = [];
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');
155 if ($cid) {
156 $this->_contactIds = explode(',', $cid);
157 }
158 if (count($this->_contactIds) > 1) {
159 $this->_single = FALSE;
160 }
161 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
162
163 $emailAttributes = [
164 'class' => 'huge',
165 ];
166 $to = $this->add('text', 'to', ts('To'), $emailAttributes, TRUE);
167
168 $this->addEntityRef('cc_id', ts('CC'), [
169 'entity' => 'Email',
170 'multiple' => TRUE,
171 ]);
172
173 $this->addEntityRef('bcc_id', ts('BCC'), [
174 'entity' => 'Email',
175 'multiple' => TRUE,
176 ]);
177
178 if ($to->getValue()) {
179 $this->_toContactIds = $this->_contactIds = [];
180 }
181 $setDefaults = TRUE;
182 if (property_exists($this, '_context') && $this->_context === 'standalone') {
183 $setDefaults = FALSE;
184 }
185
186 $this->_allContactIds = $this->_toContactIds = $this->_contactIds;
187
188 if ($to->getValue()) {
189 foreach ($this->getEmails($to) as $value) {
190 $contactId = $value['contact_id'];
191 $email = $value['email'];
192 if ($contactId) {
193 $this->_contactIds[] = $this->_toContactIds[] = $contactId;
194 $this->_toContactEmails[] = $email;
195 $this->_allContactIds[] = $contactId;
196 }
197 }
198 $setDefaults = TRUE;
199 }
200
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;
205 }
206
207 // check if we need to setdefaults and check for valid contact emails / communication preferences
208 if (is_array($this->_allContactIds) && $setDefaults) {
209 $returnProperties = [
210 'sort_name' => 1,
211 'email' => 1,
212 'do_not_email' => 1,
213 'is_deceased' => 1,
214 'on_hold' => 1,
215 'display_name' => 1,
216 'preferred_mail_format' => 1,
217 ];
218
219 // get the details for all selected contacts ( to, cc and bcc contacts )
220 list($this->_allContactDetails) = CRM_Utils_Token::getTokenDetails($this->_allContactIds,
221 $returnProperties,
222 FALSE,
223 FALSE
224 );
225
226 // perform all validations on unique contact Ids
227 foreach (array_unique($this->_allContactIds) as $key => $contactId) {
228 $value = $this->_allContactDetails[$contactId];
229 if ($value['do_not_email'] || empty($value['email']) || !empty($value['is_deceased']) || $value['on_hold']) {
230 $this->setSuppressedEmail($contactId, $value);
231 }
232 else {
233 $email = $value['email'];
234
235 // build array's which are used to setdefaults
236 if (in_array($contactId, $this->_toContactIds)) {
237 $this->_toContactDetails[$contactId] = $this->_contactDetails[$contactId] = $this->_allContactDetails[$contactId];
238 // If a particular address has been specified as the default, use that instead of contact's primary email
239 if (!empty($this->_toEmail) && $this->_toEmail['contact_id'] == $contactId) {
240 $email = $this->_toEmail['email'];
241 }
242 $toArray[] = [
243 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
244 'id' => "$contactId::{$email}",
245 ];
246 }
247 }
248 }
249
250 if (empty($toArray)) {
251 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.'));
252 }
253 }
254
255 $this->assign('toContact', json_encode($toArray));
256
257 $this->assign('suppressedEmails', count($this->suppressedEmails));
258
259 $this->assign('totalSelectedContacts', count($this->_contactIds));
260
261 $this->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE);
262
263 $this->add('select', 'from_email_address', ts('From'), $this->_fromEmails, TRUE);
264
265 CRM_Mailing_BAO_Mailing::commonCompose($this);
266
267 // add attachments
268 CRM_Core_BAO_File::buildAttachment($this, NULL);
269
270 if ($this->_single) {
271 // also fix the user context stack
272 if ($this->_caseId) {
273 $ccid = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $this->_caseId,
274 'contact_id', 'case_id'
275 );
276 $url = CRM_Utils_System::url('civicrm/contact/view/case',
277 "&reset=1&action=view&cid={$ccid}&id={$this->_caseId}"
278 );
279 }
280 elseif ($this->_context) {
281 $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1');
282 }
283 else {
284 $url = CRM_Utils_System::url('civicrm/contact/view',
285 "&show=1&action=browse&cid={$this->_contactIds[0]}&selectedChild=activity"
286 );
287 }
288
289 $session = CRM_Core_Session::singleton();
290 $session->replaceUserContext($url);
291 $this->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
292 }
293 else {
294 $this->addDefaultButtons(ts('Send Email'), 'upload');
295 }
296
297 $fields = [
298 'followup_assignee_contact_id' => [
299 'type' => 'entityRef',
300 'label' => ts('Assigned to'),
301 'attributes' => [
302 'multiple' => TRUE,
303 'create' => TRUE,
304 'api' => ['params' => ['is_deceased' => 0]],
305 ],
306 ],
307 'followup_activity_type_id' => [
308 'type' => 'select',
309 'label' => ts('Followup Activity'),
310 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant::ActivityType(FALSE),
311 'extra' => ['class' => 'crm-select2'],
312 ],
313 'followup_activity_subject' => [
314 'type' => 'text',
315 'label' => ts('Subject'),
316 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity',
317 'subject'
318 ),
319 ],
320 ];
321
322 //add followup date
323 $this->add('datepicker', 'followup_date', ts('in'));
324
325 foreach ($fields as $field => $values) {
326 if (!empty($fields[$field])) {
327 $attribute = $values['attributes'] ?? NULL;
328 $required = !empty($values['required']);
329
330 if ($values['type'] === 'select' && empty($attribute)) {
331 $this->addSelect($field, ['entity' => 'activity'], $required);
332 }
333 elseif ($values['type'] === 'entityRef') {
334 $this->addEntityRef($field, $values['label'], $attribute, $required);
335 }
336 else {
337 $this->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values));
338 }
339 }
340 }
341
342 //Added for CRM-15984: Add campaign field
343 CRM_Campaign_BAO_Campaign::addCampaign($this);
344
345 $this->addFormRule(['CRM_Contact_Form_Task_EmailCommon', 'formRule'], $this);
346 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
347 }
348
349 /**
350 * Process the form after the input has been submitted and validated.
351 *
352 * @throws \CRM_Core_Exception
353 * @throws \CiviCRM_API3_Exception
354 * @throws \Civi\API\Exception\UnauthorizedException
355 * @throws \API_Exception
356 */
357 public function postProcess() {
358 $this->bounceIfSimpleMailLimitExceeded(count($this->_contactIds));
359
360 // check and ensure that
361 $formValues = $this->controller->exportValues($this->getName());
362 $this->submit($formValues);
363 }
364
365 /**
366 * Bounce if there are more emails than permitted.
367 *
368 * @param int $count
369 * The number of emails the user is attempting to send
370 */
371 protected function bounceIfSimpleMailLimitExceeded($count) {
372 $limit = Civi::settings()->get('simple_mail_limit');
373 if ($count > $limit) {
374 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.',
375 [1 => $limit]
376 ));
377 }
378 }
379
380 /**
381 * Submit the form values.
382 *
383 * This is also accessible for testing.
384 *
385 * @param array $formValues
386 *
387 * @throws \CRM_Core_Exception
388 * @throws \CiviCRM_API3_Exception
389 * @throws \Civi\API\Exception\UnauthorizedException
390 * @throws \API_Exception
391 */
392 public function submit($formValues) {
393 $this->saveMessageTemplate($formValues);
394
395 $from = $formValues['from_email_address'] ?? NULL;
396 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
397 // If we have had a contact email used here the value returned from the line above will be the
398 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
399 $from = CRM_Utils_Mail::formatFromAddress($from);
400
401 $ccArray = $formValues['cc_id'] ? explode(',', $formValues['cc_id']) : [];
402 $cc = $this->getEmailString($ccArray);
403 $additionalDetails = empty($ccArray) ? '' : "\ncc : " . $this->getEmailUrlString($ccArray);
404
405 $bccArray = $formValues['bcc_id'] ? explode(',', $formValues['bcc_id']) : [];
406 $bcc = $this->getEmailString($bccArray);
407 $additionalDetails .= empty($bccArray) ? '' : "\nbcc : " . $this->getEmailUrlString($bccArray);
408
409 // format contact details array to handle multiple emails from same contact
410 $formattedContactDetails = [];
411 foreach ($this->_contactIds as $key => $contactId) {
412 // if we dont have details on this contactID, we should ignore
413 // potentially this is due to the contact not wanting to receive email
414 if (!isset($this->_contactDetails[$contactId])) {
415 continue;
416 }
417 $email = $this->_toContactEmails[$key];
418 // prevent duplicate emails if same email address is selected CRM-4067
419 // we should allow same emails for different contacts
420 $details = $this->_contactDetails[$contactId];
421 $details['email'] = $email;
422 unset($details['email_id']);
423 $formattedContactDetails["{$contactId}::{$email}"] = $details;
424 }
425
426 $contributionIds = [];
427 if ($this->getVar('_contributionIds')) {
428 $contributionIds = $this->getVar('_contributionIds');
429 }
430
431 // send the mail
432 list($sent, $activityId) = CRM_Activity_BAO_Activity::sendEmail(
433 $formattedContactDetails,
434 $this->getSubject($formValues['subject']),
435 $formValues['text_message'],
436 $formValues['html_message'],
437 NULL,
438 NULL,
439 $from,
440 $this->getAttachments($formValues),
441 $cc,
442 $bcc,
443 array_keys($this->_toContactDetails),
444 $additionalDetails,
445 $contributionIds,
446 CRM_Utils_Array::value('campaign_id', $formValues),
447 $this->getVar('_caseId')
448 );
449
450 $followupStatus = '';
451 if ($sent) {
452 $followupActivity = NULL;
453 if (!empty($formValues['followup_activity_type_id'])) {
454 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
455 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
456 $params['followup_date'] = $formValues['followup_date'];
457 $params['target_contact_id'] = $this->_contactIds;
458 $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']);
459 $followupActivity = CRM_Activity_BAO_Activity::createFollowupActivity($activityId, $params);
460 $followupStatus = ts('A followup activity has been scheduled.');
461
462 if (Civi::settings()->get('activity_assignee_notification')) {
463 if ($followupActivity) {
464 $mailToFollowupContacts = [];
465 $assignee = [$followupActivity->id];
466 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment::getAssigneeNames($assignee, TRUE, FALSE);
467 foreach ($assigneeContacts as $values) {
468 $mailToFollowupContacts[$values['email']] = $values;
469 }
470
471 $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts);
472 if ($sentFollowup) {
473 $followupStatus .= '<br />' . ts('A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).');
474 }
475 }
476 }
477 }
478
479 $count_success = count($this->_toContactDetails);
480 CRM_Core_Session::setStatus(ts('One message was sent successfully. ', [
481 'plural' => '%count messages were sent successfully. ',
482 'count' => $count_success,
483 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
484 }
485
486 if (!empty($this->suppressedEmails)) {
487 $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>';
488 CRM_Core_Session::setStatus($status, ts('One Message Not Sent', [
489 'count' => count($this->suppressedEmails),
490 'plural' => '%count Messages Not Sent',
491 ]), 'info');
492 }
493
494 if (isset($this->_caseId)) {
495 // if case-id is found in the url, create case activity record
496 $cases = explode(',', $this->_caseId);
497 foreach ($cases as $key => $val) {
498 if (is_numeric($val)) {
499 $caseParams = [
500 'activity_id' => $activityId,
501 'case_id' => $val,
502 ];
503 CRM_Case_BAO_Case::processCaseActivity($caseParams);
504 }
505 }
506 }
507 }
508
509 /**
510 * Save the template if update selected.
511 *
512 * @param array $formValues
513 *
514 * @throws \CiviCRM_API3_Exception
515 * @throws \Civi\API\Exception\UnauthorizedException
516 */
517 protected function saveMessageTemplate($formValues) {
518 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
519 $messageTemplate = [
520 'msg_text' => $formValues['text_message'],
521 'msg_html' => $formValues['html_message'],
522 'msg_subject' => $formValues['subject'],
523 'is_active' => TRUE,
524 ];
525
526 if (!empty($formValues['saveTemplate'])) {
527 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
528 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
529 }
530
531 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
532 $messageTemplate['id'] = $formValues['template'];
533 unset($messageTemplate['msg_title']);
534 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
535 }
536 }
537 }
538
539 /**
540 * List available tokens for this form.
541 *
542 * @return array
543 */
544 public function listTokens() {
545 return CRM_Core_SelectValues::contactTokens();
546 }
547
548 /**
549 * Get the emails from the added element.
550 *
551 * @param HTML_QuickForm_Element $element
552 *
553 * @return array
554 */
555 protected function getEmails($element): array {
556 $allEmails = explode(',', $element->getValue());
557 $return = [];
558 foreach ($allEmails as $value) {
559 $values = explode('::', $value);
560 $return[] = ['contact_id' => $values[0], 'email' => $values[1]];
561 }
562 return $return;
563 }
564
565 /**
566 * Get the string for the email IDs.
567 *
568 * @param array $emailIDs
569 * Array of email IDs.
570 *
571 * @return string
572 * e.g. "Smith, Bob<bob.smith@example.com>".
573 *
574 * @throws \API_Exception
575 * @throws \Civi\API\Exception\UnauthorizedException
576 */
577 protected function getEmailString(array $emailIDs): string {
578 if (empty($emailIDs)) {
579 return '';
580 }
581 $emails = Email::get()
582 ->addWhere('id', 'IN', $emailIDs)
583 ->setCheckPermissions(FALSE)
584 ->setSelect(['contact_id', 'email', 'contact.sort_name', 'contact.display_name'])->execute();
585 $emailStrings = [];
586 foreach ($emails as $email) {
587 $this->contactEmails[$email['id']] = $email;
588 $emailStrings[] = '"' . $email['contact.sort_name'] . '" <' . $email['email'] . '>';
589 }
590 return implode(',', $emailStrings);
591 }
592
593 /**
594 * Get the url string.
595 *
596 * This is called after the contacts have been retrieved so we don't need to re-retrieve.
597 *
598 * @param array $emailIDs
599 *
600 * @return string
601 * e.g. <a href='{$contactURL}'>Bob Smith</a>'
602 */
603 protected function getEmailUrlString(array $emailIDs): string {
604 $urlString = '';
605 foreach ($emailIDs as $email) {
606 $contactURL = CRM_Utils_System::url('civicrm/contact/view', ['reset' => 1, 'force' => 1, 'cid' => $this->contactEmails[$email]['contact_id']], TRUE);
607 $urlString .= "<a href='{$contactURL}'>" . $this->contactEmails[$email]['contact.display_name'] . '</a>';
608 }
609 return $urlString;
610 }
611
612 /**
613 * Set the emails that are not to be sent out.
614 *
615 * @param int $contactID
616 * @param array $values
617 */
618 protected function setSuppressedEmail($contactID, $values) {
619 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', 'reset=1&cid=' . $contactID);
620 $this->suppressedEmails[$contactID] = "<a href='$contactViewUrl' title='{$values['email']}'>{$values['display_name']}</a>" . ($values['on_hold'] ? '(' . ts('on hold') . ')' : '');
621 }
622
623 /**
624 * Get any attachments.
625 *
626 * @param array $formValues
627 *
628 * @return array
629 */
630 protected function getAttachments(array $formValues): array {
631 $attachments = [];
632 CRM_Core_BAO_File::formatAttachment($formValues,
633 $attachments,
634 NULL, NULL
635 );
636 return $attachments;
637 }
638
639 /**
640 * Get the subject for the message.
641 *
642 * The case handling should possibly be on the case form.....
643 *
644 * @param string $subject
645 *
646 * @return string
647 */
648 protected function getSubject(string $subject):string {
649 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
650 if (isset($this->_caseId) && is_numeric($this->_caseId)) {
651 $hash = substr(sha1(CIVICRM_SITE_KEY . $this->_caseId), 0, 7);
652 $subject = "[case #$hash] $subject";
653 }
654 return $subject;
655 }
656
657 }