[NFC] Code formatting
[civicrm-core.git] / CRM / Contact / Form / Task / EmailCommon.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 /**
19 * This class provides the common functionality for sending email to
20 * one or a group of contact ids. This class is reused by all the search
21 * components in CiviCRM (since they all have send email as a task)
22 */
23 class CRM_Contact_Form_Task_EmailCommon {
24
25 const MAX_EMAILS_KILL_SWITCH = 50;
26
27 public $_contactDetails = [];
28
29 public $_allContactDetails = [];
30
31 public $_toContactEmails = [];
32
33 /**
34 * @return array $domainEmails;
35 * @deprecated Generate an array of Domain email addresses.
36 */
37 public static function domainEmails() {
38 CRM_Core_Error::deprecatedFunctionWarning('CRM_Core_BAO_Email::domainEmails()');
39 return CRM_Core_BAO_Email::domainEmails();
40 }
41
42 /**
43 * Pre Process Form Addresses to be used in Quickform
44 *
45 * @param CRM_Core_Form $form
46 * @param bool $bounce determine if we want to throw a status bounce.
47 *
48 * @throws \CiviCRM_API3_Exception
49 */
50 public static function preProcessFromAddress(&$form, $bounce = TRUE) {
51 $form->_single = FALSE;
52 $className = CRM_Utils_System::getClassName($form);
53 if (property_exists($form, '_context') &&
54 $form->_context != 'search' &&
55 $className == 'CRM_Contact_Form_Task_Email'
56 ) {
57 $form->_single = TRUE;
58 }
59
60 $form->_emails = [];
61
62 // @TODO remove these line and to it somewhere more appropriate. Currently some classes (e.g Case
63 // are having to re-write contactIds afterwards due to this inappropriate variable setting
64 // If we don't have any contact IDs, use the logged in contact ID
65 $form->_contactIds = $form->_contactIds ?: [CRM_Core_Session::getLoggedInContactID()];
66
67 $fromEmailValues = CRM_Core_BAO_Email::getFromEmail();
68
69 $form->_noEmails = FALSE;
70 if (empty($fromEmailValues)) {
71 $form->_noEmails = TRUE;
72 }
73 $form->assign('noEmails', $form->_noEmails);
74
75 if ($bounce) {
76 if ($form->_noEmails) {
77 CRM_Core_Error::statusBounce(ts('Your user record does not have a valid email address and no from addresses have been configured.'));
78 }
79 }
80
81 $form->_emails = $fromEmailValues;
82 $defaults = [];
83 $form->_fromEmails = $fromEmailValues;
84 if (!Civi::settings()->get('allow_mail_from_logged_in_contact')) {
85 $defaults['from_email_address'] = current(CRM_Core_BAO_Domain::getNameAndEmail(FALSE, TRUE));
86 }
87 if (is_numeric(key($form->_fromEmails))) {
88 // Add signature
89 $defaultEmail = civicrm_api3('email', 'getsingle', ['id' => key($form->_fromEmails)]);
90 $defaults = [];
91 if (!empty($defaultEmail['signature_html'])) {
92 $defaults['html_message'] = '<br/><br/>--' . $defaultEmail['signature_html'];
93 }
94 if (!empty($defaultEmail['signature_text'])) {
95 $defaults['text_message'] = "\n\n--\n" . $defaultEmail['signature_text'];
96 }
97 }
98 $form->setDefaults($defaults);
99 }
100
101 /**
102 * Build the form object.
103 *
104 * @param CRM_Core_Form $form
105 *
106 * @throws \CRM_Core_Exception
107 */
108 public static function buildQuickForm(&$form) {
109 $toArray = $ccArray = $bccArray = [];
110 $suppressedEmails = 0;
111 //here we are getting logged in user id as array but we need target contact id. CRM-5988
112 $cid = $form->get('cid');
113 if ($cid) {
114 $form->_contactIds = explode(',', $cid);
115 }
116 if (count($form->_contactIds) > 1) {
117 $form->_single = FALSE;
118 }
119 CRM_Contact_Form_Task_EmailCommon::bounceIfSimpleMailLimitExceeded(count($form->_contactIds));
120
121 $emailAttributes = [
122 'class' => 'huge',
123 ];
124 $to = $form->add('text', 'to', ts('To'), $emailAttributes, TRUE);
125 $cc = $form->add('text', 'cc_id', ts('CC'), $emailAttributes);
126 $bcc = $form->add('text', 'bcc_id', ts('BCC'), $emailAttributes);
127
128 $setDefaults = TRUE;
129 if (property_exists($form, '_context') && $form->_context == 'standalone') {
130 $setDefaults = FALSE;
131 }
132
133 $elements = ['to', 'cc', 'bcc'];
134 $form->_allContactIds = $form->_toContactIds = $form->_contactIds;
135 foreach ($elements as $element) {
136 if ($$element->getValue()) {
137 $allEmails = explode(',', $$element->getValue());
138 if ($element == 'to') {
139 $form->_toContactIds = $form->_contactIds = [];
140 }
141
142 foreach ($allEmails as $value) {
143 list($contactId, $email) = explode('::', $value);
144 if ($contactId) {
145 switch ($element) {
146 case 'to':
147 $form->_contactIds[] = $form->_toContactIds[] = $contactId;
148 $form->_toContactEmails[] = $email;
149 break;
150
151 case 'cc':
152 $form->_ccContactIds[] = $contactId;
153 break;
154
155 case 'bcc':
156 $form->_bccContactIds[] = $contactId;
157 break;
158 }
159
160 $form->_allContactIds[] = $contactId;
161 }
162 }
163
164 $setDefaults = TRUE;
165 }
166 }
167
168 //get the group of contacts as per selected by user in case of Find Activities
169 if (!empty($form->_activityHolderIds)) {
170 $contact = $form->get('contacts');
171 $form->_allContactIds = $form->_contactIds = $contact;
172 }
173
174 // check if we need to setdefaults and check for valid contact emails / communication preferences
175 if (is_array($form->_allContactIds) && $setDefaults) {
176 $returnProperties = [
177 'sort_name' => 1,
178 'email' => 1,
179 'do_not_email' => 1,
180 'is_deceased' => 1,
181 'on_hold' => 1,
182 'display_name' => 1,
183 'preferred_mail_format' => 1,
184 ];
185
186 // get the details for all selected contacts ( to, cc and bcc contacts )
187 list($form->_contactDetails) = CRM_Utils_Token::getTokenDetails($form->_allContactIds,
188 $returnProperties,
189 FALSE,
190 FALSE
191 );
192
193 // make a copy of all contact details
194 $form->_allContactDetails = $form->_contactDetails;
195
196 // perform all validations on unique contact Ids
197 foreach (array_unique($form->_allContactIds) as $key => $contactId) {
198 $value = $form->_contactDetails[$contactId];
199 if ($value['do_not_email'] || empty($value['email']) || !empty($value['is_deceased']) || $value['on_hold']) {
200 $suppressedEmails++;
201
202 // unset contact details for contacts that we won't be sending email. This is prevent extra computation
203 // during token evaluation etc.
204 unset($form->_contactDetails[$contactId]);
205 }
206 else {
207 $email = $value['email'];
208
209 // build array's which are used to setdefaults
210 if (in_array($contactId, $form->_toContactIds)) {
211 $form->_toContactDetails[$contactId] = $form->_contactDetails[$contactId];
212 // If a particular address has been specified as the default, use that instead of contact's primary email
213 if (!empty($form->_toEmail) && $form->_toEmail['contact_id'] == $contactId) {
214 $email = $form->_toEmail['email'];
215 }
216 $toArray[] = [
217 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
218 'id' => "$contactId::{$email}",
219 ];
220 }
221 elseif (in_array($contactId, $form->_ccContactIds)) {
222 $ccArray[] = [
223 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
224 'id' => "$contactId::{$email}",
225 ];
226 }
227 elseif (in_array($contactId, $form->_bccContactIds)) {
228 $bccArray[] = [
229 'text' => '"' . $value['sort_name'] . '" <' . $email . '>',
230 'id' => "$contactId::{$email}",
231 ];
232 }
233 }
234 }
235
236 if (empty($toArray)) {
237 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.'));
238 }
239 }
240
241 $form->assign('toContact', json_encode($toArray));
242 $form->assign('ccContact', json_encode($ccArray));
243 $form->assign('bccContact', json_encode($bccArray));
244
245 $form->assign('suppressedEmails', $suppressedEmails);
246
247 $form->assign('totalSelectedContacts', count($form->_contactIds));
248
249 $form->add('text', 'subject', ts('Subject'), 'size=50 maxlength=254', TRUE);
250
251 $form->add('select', 'from_email_address', ts('From'), $form->_fromEmails, TRUE);
252
253 CRM_Mailing_BAO_Mailing::commonCompose($form);
254
255 // add attachments
256 CRM_Core_BAO_File::buildAttachment($form, NULL);
257
258 if ($form->_single) {
259 // also fix the user context stack
260 if ($form->_caseId) {
261 $ccid = CRM_Core_DAO::getFieldValue('CRM_Case_DAO_CaseContact', $form->_caseId,
262 'contact_id', 'case_id'
263 );
264 $url = CRM_Utils_System::url('civicrm/contact/view/case',
265 "&reset=1&action=view&cid={$ccid}&id={$form->_caseId}"
266 );
267 }
268 elseif ($form->_context) {
269 $url = CRM_Utils_System::url('civicrm/dashboard', 'reset=1');
270 }
271 else {
272 $url = CRM_Utils_System::url('civicrm/contact/view',
273 "&show=1&action=browse&cid={$form->_contactIds[0]}&selectedChild=activity"
274 );
275 }
276
277 $session = CRM_Core_Session::singleton();
278 $session->replaceUserContext($url);
279 $form->addDefaultButtons(ts('Send Email'), 'upload', 'cancel');
280 }
281 else {
282 $form->addDefaultButtons(ts('Send Email'), 'upload');
283 }
284
285 $fields = [
286 'followup_assignee_contact_id' => [
287 'type' => 'entityRef',
288 'label' => ts('Assigned to'),
289 'attributes' => [
290 'multiple' => TRUE,
291 'create' => TRUE,
292 'api' => ['params' => ['is_deceased' => 0]],
293 ],
294 ],
295 'followup_activity_type_id' => [
296 'type' => 'select',
297 'label' => ts('Followup Activity'),
298 'attributes' => ['' => '- ' . ts('select activity') . ' -'] + CRM_Core_PseudoConstant::ActivityType(FALSE),
299 'extra' => ['class' => 'crm-select2'],
300 ],
301 'followup_activity_subject' => [
302 'type' => 'text',
303 'label' => ts('Subject'),
304 'attributes' => CRM_Core_DAO::getAttribute('CRM_Activity_DAO_Activity',
305 'subject'
306 ),
307 ],
308 ];
309
310 //add followup date
311 $form->add('datepicker', 'followup_date', ts('in'));
312
313 foreach ($fields as $field => $values) {
314 if (!empty($fields[$field])) {
315 $attribute = $values['attributes'] ?? NULL;
316 $required = !empty($values['required']);
317
318 if ($values['type'] == 'select' && empty($attribute)) {
319 $form->addSelect($field, ['entity' => 'activity'], $required);
320 }
321 elseif ($values['type'] == 'entityRef') {
322 $form->addEntityRef($field, $values['label'], $attribute, $required);
323 }
324 else {
325 $form->add($values['type'], $field, $values['label'], $attribute, $required, CRM_Utils_Array::value('extra', $values));
326 }
327 }
328 }
329
330 //Added for CRM-15984: Add campaign field
331 CRM_Campaign_BAO_Campaign::addCampaign($form);
332
333 $form->addFormRule(['CRM_Contact_Form_Task_EmailCommon', 'formRule'], $form);
334 CRM_Core_Resources::singleton()->addScriptFile('civicrm', 'templates/CRM/Contact/Form/Task/EmailCommon.js', 0, 'html-header');
335 }
336
337 /**
338 * Form rule.
339 *
340 * @param array $fields
341 * The input form values.
342 * @param array $dontCare
343 * @param array $self
344 * Additional values form 'this'.
345 *
346 * @return bool|array
347 * true if no errors, else array of errors
348 */
349 public static function formRule($fields, $dontCare, $self) {
350 $errors = [];
351 $template = CRM_Core_Smarty::singleton();
352
353 if (isset($fields['html_message'])) {
354 $htmlMessage = str_replace(["\n", "\r"], ' ', $fields['html_message']);
355 $htmlMessage = str_replace('"', '\"', $htmlMessage);
356 $template->assign('htmlContent', $htmlMessage);
357 }
358
359 //Added for CRM-1393
360 if (!empty($fields['saveTemplate']) && empty($fields['saveTemplateName'])) {
361 $errors['saveTemplateName'] = ts("Enter name to save message template");
362 }
363
364 return empty($errors) ? TRUE : $errors;
365 }
366
367 /**
368 * Process the form after the input has been submitted and validated.
369 *
370 * @param CRM_Core_Form $form
371 *
372 * @throws \CRM_Core_Exception
373 * @throws \CiviCRM_API3_Exception
374 * @throws \Civi\API\Exception\UnauthorizedException
375 */
376 public static function postProcess(&$form) {
377 self::bounceIfSimpleMailLimitExceeded(count($form->_contactIds));
378
379 // check and ensure that
380 $formValues = $form->controller->exportValues($form->getName());
381 self::submit($form, $formValues);
382 }
383
384 /**
385 * Submit the form values.
386 *
387 * This is also accessible for testing.
388 *
389 * @param CRM_Core_Form $form
390 * @param array $formValues
391 *
392 * @throws \CRM_Core_Exception
393 * @throws \CiviCRM_API3_Exception
394 * @throws \Civi\API\Exception\UnauthorizedException
395 */
396 public static function submit(&$form, $formValues) {
397 self::saveMessageTemplate($formValues);
398
399 $from = $formValues['from_email_address'] ?? NULL;
400 // dev/core#357 User Emails are keyed by their id so that the Signature is able to be added
401 // If we have had a contact email used here the value returned from the line above will be the
402 // numerical key where as $from for use in the sendEmail in Activity needs to be of format of "To Name" <toemailaddress>
403 $from = CRM_Utils_Mail::formatFromAddress($from);
404 $subject = $formValues['subject'];
405
406 // CRM-13378: Append CC and BCC information at the end of Activity Details and format cc and bcc fields
407 $elements = ['cc_id', 'bcc_id'];
408 $additionalDetails = NULL;
409 $ccValues = $bccValues = [];
410 foreach ($elements as $element) {
411 if (!empty($formValues[$element])) {
412 $allEmails = explode(',', $formValues[$element]);
413 foreach ($allEmails as $value) {
414 list($contactId, $email) = explode('::', $value);
415 $contactURL = CRM_Utils_System::url('civicrm/contact/view', "reset=1&force=1&cid={$contactId}", TRUE);
416 switch ($element) {
417 case 'cc_id':
418 $ccValues['email'][] = '"' . $form->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>';
419 $ccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails[$contactId]['display_name'] . "</a>";
420 break;
421
422 case 'bcc_id':
423 $bccValues['email'][] = '"' . $form->_contactDetails[$contactId]['sort_name'] . '" <' . $email . '>';
424 $bccValues['details'][] = "<a href='{$contactURL}'>" . $form->_contactDetails[$contactId]['display_name'] . "</a>";
425 break;
426 }
427 }
428 }
429 }
430
431 $cc = $bcc = '';
432 if (!empty($ccValues)) {
433 $cc = implode(',', $ccValues['email']);
434 $additionalDetails .= "\ncc : " . implode(", ", $ccValues['details']);
435 }
436 if (!empty($bccValues)) {
437 $bcc = implode(',', $bccValues['email']);
438 $additionalDetails .= "\nbcc : " . implode(", ", $bccValues['details']);
439 }
440
441 // CRM-5916: prepend case id hash to CiviCase-originating emails’ subjects
442 if (isset($form->_caseId) && is_numeric($form->_caseId)) {
443 $hash = substr(sha1(CIVICRM_SITE_KEY . $form->_caseId), 0, 7);
444 $subject = "[case #$hash] $subject";
445 }
446
447 $attachments = [];
448 CRM_Core_BAO_File::formatAttachment($formValues,
449 $attachments,
450 NULL, NULL
451 );
452
453 // format contact details array to handle multiple emails from same contact
454 $formattedContactDetails = [];
455 $tempEmails = [];
456 foreach ($form->_contactIds as $key => $contactId) {
457 // if we dont have details on this contactID, we should ignore
458 // potentially this is due to the contact not wanting to receive email
459 if (!isset($form->_contactDetails[$contactId])) {
460 continue;
461 }
462 $email = $form->_toContactEmails[$key];
463 // prevent duplicate emails if same email address is selected CRM-4067
464 // we should allow same emails for different contacts
465 $emailKey = "{$contactId}::{$email}";
466 if (!in_array($emailKey, $tempEmails)) {
467 $tempEmails[] = $emailKey;
468 $details = $form->_contactDetails[$contactId];
469 $details['email'] = $email;
470 unset($details['email_id']);
471 $formattedContactDetails[] = $details;
472 }
473 }
474
475 $contributionIds = [];
476 if ($form->getVar('_contributionIds')) {
477 $contributionIds = $form->getVar('_contributionIds');
478 }
479
480 // send the mail
481 list($sent, $activityId) = CRM_Activity_BAO_Activity::sendEmail(
482 $formattedContactDetails,
483 $subject,
484 $formValues['text_message'],
485 $formValues['html_message'],
486 NULL,
487 NULL,
488 $from,
489 $attachments,
490 $cc,
491 $bcc,
492 array_keys($form->_toContactDetails),
493 $additionalDetails,
494 $contributionIds,
495 CRM_Utils_Array::value('campaign_id', $formValues),
496 $form->getVar('_caseId')
497 );
498
499 $followupStatus = '';
500 if ($sent) {
501 $followupActivity = NULL;
502 if (!empty($formValues['followup_activity_type_id'])) {
503 $params['followup_activity_type_id'] = $formValues['followup_activity_type_id'];
504 $params['followup_activity_subject'] = $formValues['followup_activity_subject'];
505 $params['followup_date'] = $formValues['followup_date'];
506 $params['target_contact_id'] = $form->_contactIds;
507 $params['followup_assignee_contact_id'] = explode(',', $formValues['followup_assignee_contact_id']);
508 $followupActivity = CRM_Activity_BAO_Activity::createFollowupActivity($activityId, $params);
509 $followupStatus = ts('A followup activity has been scheduled.');
510
511 if (Civi::settings()->get('activity_assignee_notification')) {
512 if ($followupActivity) {
513 $mailToFollowupContacts = [];
514 $assignee = [$followupActivity->id];
515 $assigneeContacts = CRM_Activity_BAO_ActivityAssignment::getAssigneeNames($assignee, TRUE, FALSE);
516 foreach ($assigneeContacts as $values) {
517 $mailToFollowupContacts[$values['email']] = $values;
518 }
519
520 $sentFollowup = CRM_Activity_BAO_Activity::sendToAssignee($followupActivity, $mailToFollowupContacts);
521 if ($sentFollowup) {
522 $followupStatus .= '<br />' . ts("A copy of the follow-up activity has also been sent to follow-up assignee contacts(s).");
523 }
524 }
525 }
526 }
527
528 $count_success = count($form->_toContactDetails);
529 CRM_Core_Session::setStatus(ts('One message was sent successfully. ', [
530 'plural' => '%count messages were sent successfully. ',
531 'count' => $count_success,
532 ]) . $followupStatus, ts('Message Sent', ['plural' => 'Messages Sent', 'count' => $count_success]), 'success');
533 }
534
535 // Display the name and number of contacts for those email is not sent.
536 // php 5.4 throws out a notice since the values of these below arrays are arrays.
537 // the behavior is not documented in the php manual, but it does the right thing
538 // suppressing the notices to get things in good shape going forward
539 $emailsNotSent = @array_diff_assoc($form->_allContactDetails, $form->_contactDetails);
540
541 if ($emailsNotSent) {
542 $not_sent = [];
543 foreach ($emailsNotSent as $contactId => $values) {
544 $displayName = $values['display_name'];
545 $email = $values['email'];
546 $contactViewUrl = CRM_Utils_System::url('civicrm/contact/view', "reset=1&cid=$contactId");
547 $not_sent[] = "<a href='$contactViewUrl' title='$email'>$displayName</a>" . ($values['on_hold'] ? '(' . ts('on hold') . ')' : '');
548 }
549 $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>', $not_sent) . '</li></ul>';
550 CRM_Core_Session::setStatus($status, ts('One Message Not Sent', [
551 'count' => count($emailsNotSent),
552 'plural' => '%count Messages Not Sent',
553 ]), 'info');
554 }
555
556 if (isset($form->_caseId)) {
557 // if case-id is found in the url, create case activity record
558 $cases = explode(',', $form->_caseId);
559 foreach ($cases as $key => $val) {
560 if (is_numeric($val)) {
561 $caseParams = [
562 'activity_id' => $activityId,
563 'case_id' => $val,
564 ];
565 CRM_Case_BAO_Case::processCaseActivity($caseParams);
566 }
567 }
568 }
569 }
570
571 /**
572 * Save the template if update selected.
573 *
574 * @param array $formValues
575 *
576 * @throws \CiviCRM_API3_Exception
577 * @throws \Civi\API\Exception\UnauthorizedException
578 */
579 protected static function saveMessageTemplate($formValues) {
580 if (!empty($formValues['saveTemplate']) || !empty($formValues['updateTemplate'])) {
581 $messageTemplate = [
582 'msg_text' => $formValues['text_message'],
583 'msg_html' => $formValues['html_message'],
584 'msg_subject' => $formValues['subject'],
585 'is_active' => TRUE,
586 ];
587
588 if (!empty($formValues['saveTemplate'])) {
589 $messageTemplate['msg_title'] = $formValues['saveTemplateName'];
590 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
591 }
592
593 if (!empty($formValues['template']) && !empty($formValues['updateTemplate'])) {
594 $messageTemplate['id'] = $formValues['template'];
595 unset($messageTemplate['msg_title']);
596 CRM_Core_BAO_MessageTemplate::add($messageTemplate);
597 }
598 }
599 }
600
601 /**
602 * Bounce if there are more emails than permitted.
603 *
604 * @param int $count
605 * The number of emails the user is attempting to send
606 */
607 public static function bounceIfSimpleMailLimitExceeded($count) {
608 $limit = Civi::settings()->get('simple_mail_limit');
609 if ($count > $limit) {
610 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.',
611 [1 => $limit]
612 ));
613 }
614 }
615
616 }