1 <?php
2 /**
3 * @package Joomla.Platform
4 * @subpackage Mail
5 *
6 * @copyright Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
7 * @license GNU General Public License version 2 or later; see LICENSE
8 */
9
10 defined('JPATH_PLATFORM') or die;
11
12 /**
13 * Email Class. Provides a common interface to send email from the Joomla! Platform
14 *
15 * @since 11.1
16 */
17 class JMail extends PHPMailer
18 {
19 /**
20 * JMail instances container.
21 *
22 * @var JMail[]
23 * @since 11.3
24 */
25 protected static $instances = array();
26
27 /**
28 * Charset of the message.
29 *
30 * @var string
31 * @since 11.1
32 */
33 public $CharSet = 'utf-8';
34
35 /**
36 * Constructor
37 *
38 * @param boolean $exceptions Flag if Exceptions should be thrown
39 *
40 * @since 11.1
41 */
42 public function __construct($exceptions = true)
43 {
44 parent::__construct($exceptions);
45
46 // PHPMailer has an issue using the relative path for its language files
47 $this->setLanguage('joomla', __DIR__ . '/language');
48
49 // Configure a callback function to handle errors when $this->edebug() is called
50 $this->Debugoutput = function ($message, $level)
51 {
52 JLog::add(sprintf('Error in JMail API: %s', $message), JLog::ERROR, 'mail');
53 };
54
55 // If debug mode is enabled then set SMTPDebug to the maximum level
56 if (defined('JDEBUG') && JDEBUG)
57 {
58 $this->SMTPDebug = 4;
59 }
60
61 // Don't disclose the PHPMailer version
62 $this->XMailer = ' ';
63 }
64
65 /**
66 * Returns the global email object, only creating it if it doesn't already exist.
67 *
68 * NOTE: If you need an instance to use that does not have the global configuration
69 * values, use an id string that is not 'Joomla'.
70 *
71 * @param string $id The id string for the JMail instance [optional]
72 * @param boolean $exceptions Flag if Exceptions should be thrown [optional]
73 *
74 * @return JMail The global JMail object
75 *
76 * @since 11.1
77 */
78 public static function getInstance($id = 'Joomla', $exceptions = true)
79 {
80 if (empty(self::$instances[$id]))
81 {
82 self::$instances[$id] = new JMail($exceptions);
83 }
84
85 return self::$instances[$id];
86 }
87
88 /**
89 * Send the mail
90 *
91 * @return boolean|JException Boolean true if successful, boolean false if the `mailonline` configuration is set to 0,
92 * or a JException object if the mail function does not exist or sending the message fails.
93 *
94 * @since 11.1
95 * @throws RuntimeException
96 */
97 public function Send()
98 {
99 if (JFactory::getConfig()->get('mailonline', 1))
100 {
101 if (($this->Mailer == 'mail') && !function_exists('mail'))
102 {
103 return JError::raiseNotice(500, JText::_('JLIB_MAIL_FUNCTION_DISABLED'));
104 }
105
106 try
107 {
108 // Try sending with default settings
109 $result = parent::send();
110 }
111 catch (phpmailerException $e)
112 {
113 $result = false;
114
115 if ($this->SMTPAutoTLS)
116 {
117 /**
118 * PHPMailer has an issue with servers with invalid certificates
119 *
120 * See: https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting#opportunistic-tls
121 */
122 $this->SMTPAutoTLS = false;
123
124 try
125 {
126 // Try it again with TLS turned off
127 $result = parent::send();
128 }
129 catch (phpmailerException $e)
130 {
131 // Keep false for B/C compatibility
132 $result = false;
133 }
134 }
135 }
136
137 if ($result == false)
138 {
139 $result = JError::raiseNotice(500, JText::_($this->ErrorInfo));
140 }
141
142 return $result;
143 }
144
145 JFactory::getApplication()->enqueueMessage(JText::_('JLIB_MAIL_FUNCTION_OFFLINE'));
146
147 return false;
148 }
149
150 /**
151 * Set the From and FromName properties.
152 *
153 * @param string $address The sender email address
154 * @param string $name The sender name
155 * @param boolean $auto Whether to also set the Sender address, defaults to true
156 *
157 * @return boolean
158 *
159 * @since 11.1
160 */
161 public function setFrom($address, $name = '', $auto = true)
162 {
163 try
164 {
165 if (parent::setFrom($address, $name, $auto) === false)
166 {
167 return false;
168 }
169 }
170 catch (phpmailerException $e)
171 {
172 // The parent method will have already called the logging callback, just log our deprecated error handling message
173 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
174
175 return false;
176 }
177 }
178
179 /**
180 * Set the email sender
181 *
182 * @param mixed $from email address and Name of sender
183 * <code>array([0] => email Address, [1] => Name)</code>
184 * or as a string
185 *
186 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
187 *
188 * @since 11.1
189 * @throws UnexpectedValueException
190 */
191 public function setSender($from)
192 {
193 // Wrapped in try/catch if PHPMailer is configured to throw exceptions
194 try
195 {
196 if (is_array($from))
197 {
198 // If $from is an array we assume it has an address and a name
199 if (isset($from[2]))
200 {
201 // If it is an array with entries, use them
202 $result = $this->setFrom(JMailHelper::cleanLine($from[0]), JMailHelper::cleanLine($from[1]), (bool) $from[2]);
203 }
204 else
205 {
206 $result = $this->setFrom(JMailHelper::cleanLine($from[0]), JMailHelper::cleanLine($from[1]));
207 }
208 }
209 elseif (is_string($from))
210 {
211 // If it is a string we assume it is just the address
212 $result = $this->setFrom(JMailHelper::cleanLine($from));
213 }
214 else
215 {
216 // If it is neither, we log a message and throw an exception
217 JLog::add(JText::sprintf('JLIB_MAIL_INVALID_EMAIL_SENDER', $from), JLog::WARNING, 'jerror');
218
219 throw new UnexpectedValueException(sprintf('Invalid email Sender: %s, JMail::setSender(%s)', $from));
220 }
221
222 // Check for boolean false return if exception handling is disabled
223 if ($result === false)
224 {
225 return false;
226 }
227 }
228 catch (phpmailerException $e)
229 {
230 // The parent method will have already called the logging callback, just log our deprecated error handling message
231 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
232
233 return false;
234 }
235
236 return $this;
237 }
238
239 /**
240 * Set the email subject
241 *
242 * @param string $subject Subject of the email
243 *
244 * @return JMail Returns this object for chaining.
245 *
246 * @since 11.1
247 */
248 public function setSubject($subject)
249 {
250 $this->Subject = JMailHelper::cleanLine($subject);
251
252 return $this;
253 }
254
255 /**
256 * Set the email body
257 *
258 * @param string $content Body of the email
259 *
260 * @return JMail Returns this object for chaining.
261 *
262 * @since 11.1
263 */
264 public function setBody($content)
265 {
266 /*
267 * Filter the Body
268 * TODO: Check for XSS
269 */
270 $this->Body = JMailHelper::cleanText($content);
271
272 return $this;
273 }
274
275 /**
276 * Add recipients to the email.
277 *
278 * @param mixed $recipient Either a string or array of strings [email address(es)]
279 * @param mixed $name Either a string or array of strings [name(s)]
280 * @param string $method The parent method's name.
281 *
282 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
283 *
284 * @since 11.1
285 * @throws InvalidArgumentException
286 */
287 protected function add($recipient, $name = '', $method = 'addAddress')
288 {
289 $method = lcfirst($method);
290
291 // If the recipient is an array, add each recipient... otherwise just add the one
292 if (is_array($recipient))
293 {
294 if (is_array($name))
295 {
296 $combined = array_combine($recipient, $name);
297
298 if ($combined === false)
299 {
300 throw new InvalidArgumentException("The number of elements for each array isn't equal.");
301 }
302
303 foreach ($combined as $recipientEmail => $recipientName)
304 {
305 $recipientEmail = JMailHelper::cleanLine($recipientEmail);
306 $recipientName = JMailHelper::cleanLine($recipientName);
307
308 // Wrapped in try/catch if PHPMailer is configured to throw exceptions
309 try
310 {
311 // Check for boolean false return if exception handling is disabled
312 if (call_user_func('parent::' . $method, $recipientEmail, $recipientName) === false)
313 {
314 return false;
315 }
316 }
317 catch (phpmailerException $e)
318 {
319 // The parent method will have already called the logging callback, just log our deprecated error handling message
320 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
321
322 return false;
323 }
324 }
325 }
326 else
327 {
328 $name = JMailHelper::cleanLine($name);
329
330 foreach ($recipient as $to)
331 {
332 $to = JMailHelper::cleanLine($to);
333
334 // Wrapped in try/catch if PHPMailer is configured to throw exceptions
335 try
336 {
337 // Check for boolean false return if exception handling is disabled
338 if (call_user_func('parent::' . $method, $to, $name) === false)
339 {
340 return false;
341 }
342 }
343 catch (phpmailerException $e)
344 {
345 // The parent method will have already called the logging callback, just log our deprecated error handling message
346 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
347
348 return false;
349 }
350 }
351 }
352 }
353 else
354 {
355 $recipient = JMailHelper::cleanLine($recipient);
356
357 // Wrapped in try/catch if PHPMailer is configured to throw exceptions
358 try
359 {
360 // Check for boolean false return if exception handling is disabled
361 if (call_user_func('parent::' . $method, $recipient, $name) === false)
362 {
363 return false;
364 }
365 }
366 catch (phpmailerException $e)
367 {
368 // The parent method will have already called the logging callback, just log our deprecated error handling message
369 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
370
371 return false;
372 }
373 }
374
375 return $this;
376 }
377
378 /**
379 * Add recipients to the email
380 *
381 * @param mixed $recipient Either a string or array of strings [email address(es)]
382 * @param mixed $name Either a string or array of strings [name(s)]
383 *
384 * @return JMail|boolean Returns this object for chaining.
385 *
386 * @since 11.1
387 */
388 public function addRecipient($recipient, $name = '')
389 {
390 return $this->add($recipient, $name, 'addAddress');
391 }
392
393 /**
394 * Add carbon copy recipients to the email
395 *
396 * @param mixed $cc Either a string or array of strings [email address(es)]
397 * @param mixed $name Either a string or array of strings [name(s)]
398 *
399 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
400 *
401 * @since 11.1
402 */
403 public function addCc($cc, $name = '')
404 {
405 // If the carbon copy recipient is an array, add each recipient... otherwise just add the one
406 if (isset($cc))
407 {
408 return $this->add($cc, $name, 'addCC');
409 }
410
411 return $this;
412 }
413
414 /**
415 * Add blind carbon copy recipients to the email
416 *
417 * @param mixed $bcc Either a string or array of strings [email address(es)]
418 * @param mixed $name Either a string or array of strings [name(s)]
419 *
420 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
421 *
422 * @since 11.1
423 */
424 public function addBcc($bcc, $name = '')
425 {
426 // If the blind carbon copy recipient is an array, add each recipient... otherwise just add the one
427 if (isset($bcc))
428 {
429 return $this->add($bcc, $name, 'addBCC');
430 }
431
432 return $this;
433 }
434
435 /**
436 * Add file attachment to the email
437 *
438 * @param mixed $path Either a string or array of strings [filenames]
439 * @param mixed $name Either a string or array of strings [names]
440 * @param mixed $encoding The encoding of the attachment
441 * @param mixed $type The mime type
442 * @param string $disposition The disposition of the attachment
443 *
444 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
445 *
446 * @since 12.2
447 * @throws InvalidArgumentException
448 */
449 public function addAttachment($path, $name = '', $encoding = 'base64', $type = 'application/octet-stream', $disposition = 'attachment')
450 {
451 // If the file attachments is an array, add each file... otherwise just add the one
452 if (isset($path))
453 {
454 // Wrapped in try/catch if PHPMailer is configured to throw exceptions
455 try
456 {
457 $result = true;
458
459 if (is_array($path))
460 {
461 if (!empty($name) && count($path) != count($name))
462 {
463 throw new InvalidArgumentException('The number of attachments must be equal with the number of name');
464 }
465
466 foreach ($path as $key => $file)
467 {
468 if (!empty($name))
469 {
470 $result = parent::addAttachment($file, $name[$key], $encoding, $type, $disposition);
471 }
472 else
473 {
474 $result = parent::addAttachment($file, $name, $encoding, $type, $disposition);
475 }
476 }
477 }
478 else
479 {
480 $result = parent::addAttachment($path, $name, $encoding, $type, $disposition);
481 }
482
483 // Check for boolean false return if exception handling is disabled
484 if ($result === false)
485 {
486 return false;
487 }
488 }
489 catch (phpmailerException $e)
490 {
491 // The parent method will have already called the logging callback, just log our deprecated error handling message
492 JLog::add(__METHOD__ . '() will not catch phpmailerException objects as of 4.0.', JLog::WARNING, 'deprecated');
493
494 return false;
495 }
496 }
497
498 return $this;
499 }
500
501 /**
502 * Unset all file attachments from the email
503 *
504 * @return JMail Returns this object for chaining.
505 *
506 * @since 12.2
507 */
508 public function clearAttachments()
509 {
510 parent::clearAttachments();
511
512 return $this;
513 }
514
515 /**
516 * Unset file attachments specified by array index.
517 *
518 * @param integer $index The numerical index of the attachment to remove
519 *
520 * @return JMail Returns this object for chaining.
521 *
522 * @since 12.2
523 */
524 public function removeAttachment($index = 0)
525 {
526 if (isset($this->attachment[$index]))
527 {
528 unset($this->attachment[$index]);
529 }
530
531 return $this;
532 }
533
534 /**
535 * Add Reply to email address(es) to the email
536 *
537 * @param mixed $replyto Either a string or array of strings [email address(es)]
538 * @param mixed $name Either a string or array of strings [name(s)]
539 *
540 * @return JMail|boolean Returns this object for chaining on success or boolean false on failure.
541 *
542 * @since 11.1
543 */
544 public function addReplyTo($replyto, $name = '')
545 {
546 return $this->add($replyto, $name, 'addReplyTo');
547 }
548
549 /**
550 * Sets message type to HTML
551 *
552 * @param boolean $ishtml Boolean true or false.
553 *
554 * @return JMail Returns this object for chaining.
555 *
556 * @since 12.3
557 */
558 public function isHtml($ishtml = true)
559 {
560 parent::isHTML($ishtml);
561
562 return $this;
563 }
564
565 /**
566 * Send messages using $Sendmail.
567 *
568 * This overrides the parent class to remove the restriction on the executable's name containing the word "sendmail"
569 *
570 * @return void
571 *
572 * @since 11.1
573 */
574 public function isSendmail()
575 {
576 // Prefer the Joomla configured sendmail path and default to the configured PHP path otherwise
577 $sendmail = JFactory::getConfig()->get('sendmail', ini_get('sendmail_path'));
578
579 // And if we still don't have a path, then use the system default for Linux
580 if (empty($sendmail))
581 {
582 $sendmail = '/usr/sbin/sendmail';
583 }
584
585 $this->Sendmail = $sendmail;
586 $this->Mailer = 'sendmail';
587 }
588
589 /**
590 * Use sendmail for sending the email
591 *
592 * @param string $sendmail Path to sendmail [optional]
593 *
594 * @return boolean True on success
595 *
596 * @since 11.1
597 */
598 public function useSendmail($sendmail = null)
599 {
600 $this->Sendmail = $sendmail;
601
602 if (!empty($this->Sendmail))
603 {
604 $this->isSendmail();
605
606 return true;
607 }
608 else
609 {
610 $this->isMail();
611
612 return false;
613 }
614 }
615
616 /**
617 * Use SMTP for sending the email
618 *
619 * @param string $auth SMTP Authentication [optional]
620 * @param string $host SMTP Host [optional]
621 * @param string $user SMTP Username [optional]
622 * @param string $pass SMTP Password [optional]
623 * @param string $secure Use secure methods
624 * @param integer $port The SMTP port
625 *
626 * @return boolean True on success
627 *
628 * @since 11.1
629 */
630 public function useSmtp($auth = null, $host = null, $user = null, $pass = null, $secure = null, $port = 25)
631 {
632 $this->SMTPAuth = $auth;
633 $this->Host = $host;
634 $this->Username = $user;
635 $this->Password = $pass;
636 $this->Port = $port;
637
638 if ($secure == 'ssl' || $secure == 'tls')
639 {
640 $this->SMTPSecure = $secure;
641 }
642
643 if (($this->SMTPAuth !== null && $this->Host !== null && $this->Username !== null && $this->Password !== null)
644 || ($this->SMTPAuth === null && $this->Host !== null))
645 {
646 $this->isSMTP();
647
648 return true;
649 }
650 else
651 {
652 $this->isMail();
653
654 return false;
655 }
656 }
657
658 /**
659 * Function to send an email
660 *
661 * @param string $from From email address
662 * @param string $fromName From name
663 * @param mixed $recipient Recipient email address(es)
664 * @param string $subject email subject
665 * @param string $body Message body
666 * @param boolean $mode false = plain text, true = HTML
667 * @param mixed $cc CC email address(es)
668 * @param mixed $bcc BCC email address(es)
669 * @param mixed $attachment Attachment file name(s)
670 * @param mixed $replyTo Reply to email address(es)
671 * @param mixed $replyToName Reply to name(s)
672 *
673 * @return boolean True on success
674 *
675 * @since 11.1
676 */
677 public function sendMail($from, $fromName, $recipient, $subject, $body, $mode = false, $cc = null, $bcc = null, $attachment = null,
678 $replyTo = null, $replyToName = null)
679 {
680 // Create config object
681 $config = JFactory::getConfig();
682
683 $this->setSubject($subject);
684 $this->setBody($body);
685
686 // Are we sending the email as HTML?
687 $this->isHtml($mode);
688
689 /*
690 * Do not send the message if adding any of the below items fails
691 */
692
693 if ($this->addRecipient($recipient) === false)
694 {
695 return false;
696 }
697
698 if ($this->addCc($cc) === false)
699 {
700 return false;
701 }
702
703 if ($this->addBcc($bcc) === false)
704 {
705 return false;
706 }
707
708 if ($this->addAttachment($attachment) === false)
709 {
710 return false;
711 }
712
713 // Take care of reply email addresses
714 if (is_array($replyTo))
715 {
716 $numReplyTo = count($replyTo);
717
718 for ($i = 0; $i < $numReplyTo; $i++)
719 {
720 if ($this->addReplyTo($replyTo[$i], $replyToName[$i]) === false)
721 {
722 return false;
723 }
724 }
725 }
726 elseif (isset($replyTo))
727 {
728 if ($this->addReplyTo($replyTo, $replyToName) === false)
729 {
730 return false;
731 }
732 }
733 elseif ($config->get('replyto'))
734 {
735 $this->addReplyTo($config->get('replyto'), $config->get('replytoname'));
736 }
737
738 // Add sender to replyTo only if no replyTo received
739 $autoReplyTo = (empty($this->ReplyTo)) ? true : false;
740
741 if ($this->setSender(array($from, $fromName, $autoReplyTo)) === false)
742 {
743 return false;
744 }
745
746 return $this->Send();
747 }
748
749 /**
750 * Sends mail to administrator for approval of a user submission
751 *
752 * @param string $adminName Name of administrator
753 * @param string $adminEmail Email address of administrator
754 * @param string $email [NOT USED TODO: Deprecate?]
755 * @param string $type Type of item to approve
756 * @param string $title Title of item to approve
757 * @param string $author Author of item to approve
758 * @param string $url A URL to included in the mail
759 *
760 * @return boolean True on success
761 *
762 * @since 11.1
763 * @deprecated 4.0 Without replacement please implement it in your own code
764 */
765 public function sendAdminMail($adminName, $adminEmail, $email, $type, $title, $author, $url = null)
766 {
767 $subject = JText::sprintf('JLIB_MAIL_USER_SUBMITTED', $type);
768
769 $message = sprintf(JText::_('JLIB_MAIL_MSG_ADMIN'), $adminName, $type, $title, $author, $url, $url, 'administrator', $type);
770 $message .= JText::_('JLIB_MAIL_MSG') . "\n";
771
772 if ($this->addRecipient($adminEmail) === false)
773 {
774 return false;
775 }
776
777 $this->setSubject($subject);
778 $this->setBody($message);
779
780 return $this->Send();
781 }
782 }
783