Merge pull request #15944 from magnolia61/Sort_CMS_tables_alphabetically
[civicrm-core.git] / CRM / Utils / Mail.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 class CRM_Utils_Mail {
18
19 /**
20 * Create a new mailer to send any mail from the application.
21 *
22 * Note: The mailer is opened in persistent mode.
23 *
24 * Note: You probably don't want to call this directly. Get a reference
25 * to the mailer through the container.
26 *
27 * @return Mail
28 */
29 public static function createMailer() {
30 $mailingInfo = Civi::settings()->get('mailing_backend');
31
32 if ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB ||
33 (defined('CIVICRM_MAILER_SPOOL') && CIVICRM_MAILER_SPOOL)
34 ) {
35 $mailer = self::_createMailer('CRM_Mailing_BAO_Spool', []);
36 }
37 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_SMTP) {
38 if ($mailingInfo['smtpServer'] == '' || !$mailingInfo['smtpServer']) {
39 CRM_Core_Error::debug_log_message(ts('There is no valid smtp server setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the SMTP Server.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
40 CRM_Core_Error::fatal(ts('There is no valid smtp server setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the SMTP Server.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
41 }
42
43 $params['host'] = $mailingInfo['smtpServer'] ? $mailingInfo['smtpServer'] : 'localhost';
44 $params['port'] = $mailingInfo['smtpPort'] ? $mailingInfo['smtpPort'] : 25;
45
46 if ($mailingInfo['smtpAuth']) {
47 $params['username'] = $mailingInfo['smtpUsername'];
48 $params['password'] = CRM_Utils_Crypt::decrypt($mailingInfo['smtpPassword']);
49 $params['auth'] = TRUE;
50 }
51 else {
52 $params['auth'] = FALSE;
53 }
54
55 /*
56 * Set the localhost value, CRM-3153
57 * Use the host name of the web server, falling back to the base URL
58 * (eg when using the PHP CLI), and then falling back to localhost.
59 */
60 $params['localhost'] = CRM_Utils_Array::value(
61 'SERVER_NAME',
62 $_SERVER,
63 CRM_Utils_Array::value(
64 'host',
65 parse_url(CIVICRM_UF_BASEURL),
66 'localhost'
67 )
68 );
69
70 // also set the timeout value, lets set it to 30 seconds
71 // CRM-7510
72 $params['timeout'] = 30;
73
74 // CRM-9349
75 $params['persist'] = TRUE;
76
77 $mailer = self::_createMailer('smtp', $params);
78 }
79 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_SENDMAIL) {
80 if ($mailingInfo['sendmail_path'] == '' ||
81 !$mailingInfo['sendmail_path']
82 ) {
83 CRM_Core_Error::debug_log_message(ts('There is no valid sendmail path setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the sendmail server.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
84 CRM_Core_Error::fatal(ts('There is no valid sendmail path setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the sendmail server.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
85 }
86 $params['sendmail_path'] = $mailingInfo['sendmail_path'];
87 $params['sendmail_args'] = $mailingInfo['sendmail_args'];
88
89 $mailer = self::_createMailer('sendmail', $params);
90 }
91 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_MAIL) {
92 $mailer = self::_createMailer('mail', []);
93 }
94 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_MOCK) {
95 $mailer = self::_createMailer('mock', []);
96 }
97 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_DISABLED) {
98 CRM_Core_Error::debug_log_message(ts('Outbound mail has been disabled. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the OutBound Email.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
99 CRM_Core_Error::statusBounce(ts('Outbound mail has been disabled. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the OutBound Email.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
100 }
101 else {
102 CRM_Core_Error::debug_log_message(ts('There is no valid SMTP server Setting Or SendMail path setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the OutBound Email.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
103 CRM_Core_Error::debug_var('mailing_info', $mailingInfo);
104 CRM_Core_Error::statusBounce(ts('There is no valid SMTP server Setting Or sendMail path setting. Click <a href=\'%1\'>Administer >> System Setting >> Outbound Email</a> to set the OutBound Email.', [1 => CRM_Utils_System::url('civicrm/admin/setting/smtp', 'reset=1')]));
105 }
106 return $mailer;
107 }
108
109 /**
110 * Create a new instance of a PEAR Mail driver.
111 *
112 * @param string $driver
113 * 'CRM_Mailing_BAO_Spool' or a name suitable for Mail::factory().
114 * @param array $params
115 * @return object
116 * More specifically, a class which implements the "send()" function
117 */
118 public static function _createMailer($driver, $params) {
119 if ($driver == 'CRM_Mailing_BAO_Spool') {
120 $mailer = new CRM_Mailing_BAO_Spool($params);
121 }
122 else {
123 $mailer = Mail::factory($driver, $params);
124 }
125 CRM_Utils_Hook::alterMailer($mailer, $driver, $params);
126 return $mailer;
127 }
128
129 /**
130 * Wrapper function to send mail in CiviCRM. Hooks are called from this function. The input parameter
131 * is an associateive array which holds the values of field needed to send an email. These are:
132 *
133 * from : complete from envelope
134 * toName : name of person to send email
135 * toEmail : email address to send to
136 * cc : email addresses to cc
137 * bcc : email addresses to bcc
138 * subject : subject of the email
139 * text : text of the message
140 * html : html version of the message
141 * replyTo : reply-to header in the email
142 * attachments: an associative array of
143 * fullPath : complete pathname to the file
144 * mime_type: mime type of the attachment
145 * cleanName: the user friendly name of the attachmment
146 *
147 * @param array $params
148 * (by reference).
149 *
150 * @return bool
151 * TRUE if a mail was sent, else FALSE.
152 */
153 public static function send(&$params) {
154 $defaultReturnPath = CRM_Core_BAO_MailSettings::defaultReturnPath();
155 $includeMessageId = CRM_Core_BAO_MailSettings::includeMessageId();
156 $emailDomain = CRM_Core_BAO_MailSettings::defaultDomain();
157 $from = CRM_Utils_Array::value('from', $params);
158 if (!$defaultReturnPath) {
159 $defaultReturnPath = self::pluckEmailFromHeader($from);
160 }
161
162 // first call the mail alter hook
163 CRM_Utils_Hook::alterMailParams($params, 'singleEmail');
164
165 // check if any module has aborted mail sending
166 if (!empty($params['abortMailSend']) || empty($params['toEmail'])) {
167 return FALSE;
168 }
169
170 $textMessage = CRM_Utils_Array::value('text', $params);
171 $htmlMessage = CRM_Utils_Array::value('html', $params);
172 $attachments = CRM_Utils_Array::value('attachments', $params);
173
174 // CRM-6224
175 if (trim(CRM_Utils_String::htmlToText($htmlMessage)) == '') {
176 $htmlMessage = FALSE;
177 }
178
179 $headers = [];
180 // CRM-10699 support custom email headers
181 if (!empty($params['headers'])) {
182 $headers = array_merge($headers, $params['headers']);
183 }
184 $headers['From'] = $params['from'];
185 $headers['To'] = self::formatRFC822Email(
186 CRM_Utils_Array::value('toName', $params),
187 CRM_Utils_Array::value('toEmail', $params),
188 FALSE
189 );
190
191 // On some servers mail() fails when 'Cc' or 'Bcc' headers are defined but empty.
192 foreach (['Cc', 'Bcc'] as $optionalHeader) {
193 $headers[$optionalHeader] = CRM_Utils_Array::value(strtolower($optionalHeader), $params);
194 if (empty($headers[$optionalHeader])) {
195 unset($headers[$optionalHeader]);
196 }
197 }
198
199 $headers['Subject'] = CRM_Utils_Array::value('subject', $params);
200 $headers['Content-Type'] = $htmlMessage ? 'multipart/mixed; charset=utf-8' : 'text/plain; charset=utf-8';
201 $headers['Content-Disposition'] = 'inline';
202 $headers['Content-Transfer-Encoding'] = '8bit';
203 $headers['Return-Path'] = CRM_Utils_Array::value('returnPath', $params, $defaultReturnPath);
204
205 // CRM-11295: Omit reply-to headers if empty; this avoids issues with overzealous mailservers
206 $replyTo = CRM_Utils_Array::value('replyTo', $params, CRM_Utils_Array::value('from', $params));
207
208 if (!empty($replyTo)) {
209 $headers['Reply-To'] = $replyTo;
210 }
211 $headers['Date'] = date('r');
212 if ($includeMessageId) {
213 $headers['Message-ID'] = '<' . uniqid('civicrm_', TRUE) . "@$emailDomain>";
214 }
215 if (!empty($params['autoSubmitted'])) {
216 $headers['Auto-Submitted'] = "Auto-Generated";
217 }
218
219 // make sure we has to have space, CRM-6977
220 foreach (['From', 'To', 'Cc', 'Bcc', 'Reply-To', 'Return-Path'] as $fld) {
221 if (isset($headers[$fld])) {
222 $headers[$fld] = str_replace('"<', '" <', $headers[$fld]);
223 }
224 }
225
226 // quote FROM, if comma is detected AND is not already quoted. CRM-7053
227 if (strpos($headers['From'], ',') !== FALSE) {
228 $from = explode(' <', $headers['From']);
229 $headers['From'] = self::formatRFC822Email(
230 $from[0],
231 substr(trim($from[1]), 0, -1),
232 TRUE
233 );
234 }
235
236 require_once 'Mail/mime.php';
237 $msg = new Mail_mime("\n");
238 if ($textMessage) {
239 $msg->setTxtBody($textMessage);
240 }
241
242 if ($htmlMessage) {
243 $msg->setHTMLBody($htmlMessage);
244 }
245
246 if (!empty($attachments)) {
247 foreach ($attachments as $fileID => $attach) {
248 $msg->addAttachment(
249 $attach['fullPath'],
250 $attach['mime_type'],
251 $attach['cleanName']
252 );
253 }
254 }
255
256 $message = self::setMimeParams($msg);
257 $headers = $msg->headers($headers);
258
259 $to = [$params['toEmail']];
260 $result = NULL;
261 $mailer = \Civi::service('pear_mail');
262
263 // CRM-3795, CRM-7355, CRM-7557, CRM-9058, CRM-9887, CRM-12883, CRM-19173 and others ...
264 // The PEAR library requires different parameters based on the mailer used:
265 // * Mail_mail requires the Cc/Bcc recipients listed ONLY in the $headers variable
266 // * All other mailers require that all be recipients be listed in the $to array AND that
267 // the Bcc must not be present in $header as otherwise it will be shown to all recipients
268 // ref: https://pear.php.net/bugs/bug.php?id=8047, full thread and answer [2011-04-19 20:48 UTC]
269 if (get_class($mailer) != "Mail_mail") {
270 // get emails from headers, since these are
271 // combination of name and email addresses.
272 if (!empty($headers['Cc'])) {
273 $to[] = CRM_Utils_Array::value('Cc', $headers);
274 }
275 if (!empty($headers['Bcc'])) {
276 $to[] = CRM_Utils_Array::value('Bcc', $headers);
277 unset($headers['Bcc']);
278 }
279 }
280
281 if (is_object($mailer)) {
282 $errorScope = CRM_Core_TemporaryErrorScope::ignoreException();
283 $result = $mailer->send($to, $headers, $message);
284 if (is_a($result, 'PEAR_Error')) {
285 $message = self::errorMessage($mailer, $result);
286 // append error message in case multiple calls are being made to
287 // this method in the course of sending a batch of messages.
288 CRM_Core_Session::setStatus($message, ts('Mailing Error'), 'error');
289 return FALSE;
290 }
291 // CRM-10699
292 CRM_Utils_Hook::postEmailSend($params);
293 return TRUE;
294 }
295 return FALSE;
296 }
297
298 /**
299 * @param $mailer
300 * @param $result
301 *
302 * @return string
303 */
304 public static function errorMessage($mailer, $result) {
305 $message = '<p>' . ts('An error occurred when CiviCRM attempted to send an email (via %1). If you received this error after submitting on online contribution or event registration - the transaction was completed, but we were unable to send the email receipt.', [
306 1 => 'SMTP',
307 ]) . '</p>' . '<p>' . ts('The mail library returned the following error message:') . '<br /><span class="font-red"><strong>' . $result->getMessage() . '</strong></span></p>' . '<p>' . ts('This is probably related to a problem in your Outbound Email Settings (Administer CiviCRM &raquo; System Settings &raquo; Outbound Email), OR the FROM email address specifically configured for your contribution page or event. Possible causes are:') . '</p>';
308
309 if (is_a($mailer, 'Mail_smtp')) {
310 $message .= '<ul>' . '<li>' . ts('Your SMTP Username or Password are incorrect.') . '</li>' . '<li>' . ts('Your SMTP Server (machine) name is incorrect.') . '</li>' . '<li>' . ts('You need to use a Port other than the default port 25 in your environment.') . '</li>' . '<li>' . ts('Your SMTP server is just not responding right now (it is down for some reason).') . '</li>';
311 }
312 else {
313 $message .= '<ul>' . '<li>' . ts('Your Sendmail path is incorrect.') . '</li>' . '<li>' . ts('Your Sendmail argument is incorrect.') . '</li>';
314 }
315
316 $message .= '<li>' . ts('The FROM Email Address configured for this feature may not be a valid sender based on your email service provider rules.') . '</li>' . '</ul>' . '<p>' . ts('Check <a href="%1">this page</a> for more information.', [
317 1 => CRM_Utils_System::docURL2('user/advanced-configuration/email-system-configuration', TRUE),
318 ]) . '</p>';
319
320 return $message;
321 }
322
323 /**
324 * @param $to
325 * @param $headers
326 * @param $message
327 */
328 public static function logger(&$to, &$headers, &$message) {
329 if (is_array($to)) {
330 $toString = implode(', ', $to);
331 $fileName = $to[0];
332 }
333 else {
334 $toString = $fileName = $to;
335 }
336 $content = "To: " . $toString . "\n";
337 foreach ($headers as $key => $val) {
338 $content .= "$key: $val\n";
339 }
340 $content .= "\n" . $message . "\n";
341
342 if (is_numeric(CIVICRM_MAIL_LOG)) {
343 $config = CRM_Core_Config::singleton();
344 // create the directory if not there
345 $dirName = $config->configAndLogDir . 'mail' . DIRECTORY_SEPARATOR;
346 CRM_Utils_File::createDir($dirName);
347 $fileName = md5(uniqid(CRM_Utils_String::munge($fileName))) . '.txt';
348 file_put_contents($dirName . $fileName,
349 $content
350 );
351 }
352 else {
353 file_put_contents(CIVICRM_MAIL_LOG, $content, FILE_APPEND);
354 }
355 }
356
357 /**
358 * Get the email address itself from a formatted full name + address string
359 *
360 * Ugly but working.
361 *
362 * @param string $header
363 * The full name + email address string.
364 *
365 * @return string
366 * the plucked email address
367 */
368 public static function pluckEmailFromHeader($header) {
369 preg_match('/<([^<]*)>$/', $header, $matches);
370
371 if (isset($matches[1])) {
372 return $matches[1];
373 }
374 return NULL;
375 }
376
377 /**
378 * Get the Active outBound email.
379 *
380 * @return bool
381 * TRUE if valid outBound email configuration found, false otherwise.
382 */
383 public static function validOutBoundMail() {
384 $mailingInfo = Civi::settings()->get('mailing_backend');
385 if ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_MAIL) {
386 return TRUE;
387 }
388 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_SMTP) {
389 if (!isset($mailingInfo['smtpServer']) || $mailingInfo['smtpServer'] == '' ||
390 $mailingInfo['smtpServer'] == 'YOUR SMTP SERVER' ||
391 ($mailingInfo['smtpAuth'] && ($mailingInfo['smtpUsername'] == '' || $mailingInfo['smtpPassword'] == ''))
392 ) {
393 return FALSE;
394 }
395 return TRUE;
396 }
397 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_SENDMAIL) {
398 if (!$mailingInfo['sendmail_path'] || !$mailingInfo['sendmail_args']) {
399 return FALSE;
400 }
401 return TRUE;
402 }
403 elseif ($mailingInfo['outBound_option'] == CRM_Mailing_Config::OUTBOUND_OPTION_REDIRECT_TO_DB) {
404 return TRUE;
405 }
406 return FALSE;
407 }
408
409 /**
410 * @param $message
411 * @param array $params
412 *
413 * @return mixed
414 */
415 public static function setMimeParams($message, $params = NULL) {
416 static $mimeParams = NULL;
417 if (!$params) {
418 if (!$mimeParams) {
419 $mimeParams = [
420 'text_encoding' => '8bit',
421 'html_encoding' => '8bit',
422 'head_charset' => 'utf-8',
423 'text_charset' => 'utf-8',
424 'html_charset' => 'utf-8',
425 ];
426 }
427 $params = $mimeParams;
428 }
429 return $message->get($params);
430 }
431
432 /**
433 * @param string $name
434 * @param $email
435 * @param bool $useQuote
436 *
437 * @return null|string
438 */
439 public static function formatRFC822Email($name, $email, $useQuote = FALSE) {
440 $result = NULL;
441
442 $name = trim($name);
443
444 // strip out double quotes if present at the beginning AND end
445 if (substr($name, 0, 1) == '"' &&
446 substr($name, -1, 1) == '"'
447 ) {
448 $name = substr($name, 1, -1);
449 }
450
451 if (!empty($name)) {
452 // escape the special characters
453 $name = str_replace(['<', '"', '>'],
454 ['\<', '\"', '\>'],
455 $name
456 );
457 if (strpos($name, ',') !== FALSE ||
458 $useQuote
459 ) {
460 // quote the string if it has a comma
461 $name = '"' . $name . '"';
462 }
463
464 $result = "$name ";
465 }
466
467 $result .= "<{$email}>";
468 return $result;
469 }
470
471 /**
472 * Takes a string and checks to see if it needs to be escaped / double quoted
473 * and if so does the needful and return the formatted name
474 *
475 * This code has been copied and adapted from ezc/Mail/src/tools.php
476 *
477 * @param string $name
478 *
479 * @return string
480 */
481 public static function formatRFC2822Name($name) {
482 $name = trim($name);
483 if (!empty($name)) {
484 // remove the quotes around the name part if they are already there
485 if (substr($name, 0, 1) == '"' && substr($name, -1) == '"') {
486 $name = substr($name, 1, -1);
487 }
488
489 // add slashes to " and \ and surround the name part with quotes
490 if (strpbrk($name, ",@<>:;'\"") !== FALSE) {
491 $name = '"' . addcslashes($name, '\\"') . '"';
492 }
493 }
494
495 return $name;
496 }
497
498 /**
499 * @param string $fileName
500 * @param string $html
501 * @param string $format
502 *
503 * @return array
504 */
505 public static function appendPDF($fileName, $html, $format = NULL) {
506 $pdf_filename = CRM_Core_Config::singleton()->templateCompileDir . CRM_Utils_File::makeFileName($fileName);
507
508 // FIXME : CRM-7894
509 // xmlns attribute is required in XHTML but it is invalid in HTML,
510 // Also the namespace "xmlns=http://www.w3.org/1999/xhtml" is default,
511 // and will be added to the <html> tag even if you do not include it.
512 $html = preg_replace('/(<html)(.+?xmlns=["\'].[^\s]+["\'])(.+)?(>)/', '\1\3\4', $html);
513
514 file_put_contents($pdf_filename, CRM_Utils_PDF_Utils::html2pdf($html,
515 $fileName,
516 TRUE,
517 $format)
518 );
519 return [
520 'fullPath' => $pdf_filename,
521 'mime_type' => 'application/pdf',
522 'cleanName' => $fileName,
523 ];
524 }
525
526 /**
527 * Format an email string from email fields.
528 *
529 * @param array $fields
530 * The email fields.
531 * @return string
532 * The formatted email string.
533 */
534 public static function format($fields) {
535 $formattedEmail = '';
536 if (!empty($fields['email'])) {
537 $formattedEmail = $fields['email'];
538 }
539
540 $formattedSuffix = [];
541 if (!empty($fields['is_bulkmail'])) {
542 $formattedSuffix[] = '(' . ts('Bulk') . ')';
543 }
544 if (!empty($fields['on_hold'])) {
545 if ($fields['on_hold'] == 2) {
546 $formattedSuffix[] = '(' . ts('On Hold - Opt Out') . ')';
547 }
548 else {
549 $formattedSuffix[] = '(' . ts('On Hold') . ')';
550 }
551 }
552 if (!empty($fields['signature_html']) || !empty($fields['signature_text'])) {
553 $formattedSuffix[] = '(' . ts('Signature') . ')';
554 }
555
556 // Add suffixes on a new line, if there is any.
557 if (!empty($formattedSuffix)) {
558 $formattedEmail .= "\n" . implode(' ', $formattedSuffix);
559 }
560
561 return $formattedEmail;
562 }
563
564 /**
565 * When passed a value, returns the value if it's non-numeric.
566 * If it's numeric, look up the display name and email of the corresponding
567 * contact ID in RFC822 format.
568 *
569 * @param string $from
570 * contact ID or formatted "From address", eg. 12 or "Fred Bloggs" <fred@example.org>
571 * @return string
572 * The RFC822-formatted email header (display name + address)
573 */
574 public static function formatFromAddress($from) {
575 if (is_numeric($from)) {
576 $result = civicrm_api3('Email', 'get', [
577 'id' => $from,
578 'return' => ['contact_id.display_name', 'email'],
579 'sequential' => 1,
580 ])['values'][0];
581 $from = '"' . $result['contact_id.display_name'] . '" <' . $result['email'] . '>';
582 }
583 return $from;
584 }
585
586 }