1: <?php
2:
3: /**
4: * This file contains the cMailer class for all mail sending purposes.
5: *
6: * @package Core
7: * @subpackage Backend
8: * @author Rusmir Jusufovic
9: * @author Simon Sprankel
10: * @copyright four for business AG <www.4fb.de>
11: * @license http://www.contenido.org/license/LIZENZ.txt
12: * @link http://www.4fb.de
13: * @link http://www.contenido.org
14: */
15:
16: defined('CON_FRAMEWORK') || die('Illegal call: Missing framework initialization - request aborted.');
17:
18: // since CONTENIDO has it's own autoloader, swift_init.php is enough
19: // we do not need and should not use swift_required.php!
20: require_once 'swiftmailer/lib/swift_init.php';
21:
22: /**
23: * Mailer class for all mail sending purposes.
24: *
25: * The class cMailer is a facade for the SwiftMailer library that
26: * simplifies the process of sending mails by providing some
27: * convenience methods.
28: *
29: * <strong>Simple example</strong>
30: * <code>
31: * $mailer = new cMailer();
32: * $mailer->sendMail(null, 'recipient@contenido.org', 'subject', 'body');
33: * </code>
34: *
35: * <strong>Default sender of mails</strong>
36: * When sending a mail using the sendMail() method of the cMailer class
37: * you can give a mail sender as first parameter. This can either be an
38: * email address as string or an array with the email address as key and
39: * the senders name as value. If you pass an empty value instead the
40: * default mail sender is used. This default mail sender can be
41: * configured with the system properties system/mail_sender and
42: * system/mail_sender_name. If no default mail sender is configured it
43: * defaults to "noreply@contenido.org" and "CONTENIDO Backend".
44: *
45: * <strong>User defined mail sender example</strong>
46: * <code>
47: * $mailer->sendMail('sender@contenido.org', 'recipient@contenido.org', 'subject');
48: * $mailer->sendMail(array('sender@contenido.org' => 'sender name'), 'recipient@contenido.org', 'subject');
49: * </code>
50: *
51: * <strong>Logging mails</strong>
52: * @todo explain logging of mails via _logMail()
53: *
54: * <strong>Resending mails</strong>
55: * @todo explain resending of mails via resendMail()
56: *
57: * <strong>Sending user created messages</strong>
58: * Creating your own message is e.g. necessary in order to send mails
59: * with attachments as the simplified interface the cMailer class offers
60: * does not yet provide means to do so.
61: * @todo explain sending of user created messages via send()
62: *
63: * <strong>Default transport</strong>
64: * By default the cMailer tries to use an SMTP transport with optional
65: * authentication. If starting the SMTP transport fails, a simple MAIL
66: * transport will be used (using PHP's mail() function).
67: *
68: * <strong>User defined transport</strong>
69: * When creating a cMailer instance an arbitrary transport can be given
70: * to override the afore mentioned behaviour.
71: *
72: * <strong>User defined transport example</strong>
73: * <code>
74: * @todo add example
75: * </code>
76: *
77: * <strong>User defined character set</strong>
78: * @todo explain setCharset()
79: *
80: * @package Core
81: * @subpackage Backend
82: */
83: class cMailer extends Swift_Mailer {
84:
85: /**
86: * Mail address of the default mail sender.
87: * This will be read from system property system/mail_sender.
88: * Can be overriden by giving a sender when sending a mail.
89: *
90: * @var string
91: */
92: private $_mailSender = 'noreply@contenido.org';
93:
94: /**
95: * Name of the default mail sender.
96: * This will be read from system property system/mail_sender_name.
97: * Can be overriden by giving a sender when sending a mail.
98: *
99: * @var string
100: */
101: private $_mailSenderName = 'CONTENIDO Backend';
102:
103: /**
104: * Name of the mail host.
105: * This will be read from system property system/mail_host.
106: *
107: * @var string
108: */
109: private $_mailHost = 'localhost';
110:
111: /**
112: * Port of the mail host.
113: * This will be read from system property system/mail_port.
114: *
115: * @var int
116: */
117: private $_mailPort = 25;
118:
119: /**
120: * The mail encryption method (ssl/tls).
121: * This will be read from system property system/mail_encryption.
122: *
123: * @var string
124: */
125: private $_mailEncryption = NULL;
126:
127: /**
128: * Name of the mail host user.
129: * This will be read from system property system/mail_user.
130: * Used for authentication at the mail host.
131: *
132: * @var string
133: */
134: private $_mailUser = '';
135:
136: /**
137: * Password of the mail host user.
138: * This will be read from system property system/mail_pass.
139: * Used for authentication at the mail host.
140: *
141: * @var string
142: */
143: private $_mailPass = '';
144:
145: /**
146: * Constructor to create an instance of this class.
147: *
148: * System properties to define the default mail sender are read and
149: * aggregated.
150: *
151: * An arbitrary transport instance of class Swift_Transport can be
152: * given. If no transport is given, system properties to build a
153: * transport are read and aggregated and eventually a transport is
154: * created using constructTransport().
155: *
156: * @todo add type hinting!
157: * @param Swift_Transport $transport [optional]
158: * a transport instance
159: */
160: public function __construct($transport = NULL) {
161:
162: // get address of default mail sender
163: $mailSender = getSystemProperty('system', 'mail_sender');
164: if (Swift_Validate::email($mailSender)) {
165: $this->_mailSender = $mailSender;
166: }
167:
168: // get name of default mail sender
169: $mailSenderName = getSystemProperty('system', 'mail_sender_name');
170: if (!empty($mailSenderName)) {
171: $this->_mailSenderName = $mailSenderName;
172: }
173:
174: // if a transport object has been given, use it and skip the rest
175: if (!is_null($transport)) {
176: parent::__construct($transport);
177: return;
178: }
179: // if no transport object has been given, read system setting
180: // and create one
181:
182: // get name of mail host
183: $mailHost = getSystemProperty('system', 'mail_host');
184: if (!empty($mailHost)) {
185: $this->_mailHost = $mailHost;
186: }
187:
188: // get port of mail host
189: if (is_numeric(getSystemProperty('system', 'mail_port'))) {
190: $this->_mailPort = (int) getSystemProperty('system', 'mail_port');
191: }
192:
193: // get mail encryption
194: $encryptions = array(
195: 'tls',
196: 'ssl'
197: );
198: $mail_encryption = strtolower(getSystemProperty('system', 'mail_encryption'));
199: if (in_array($mail_encryption, $encryptions)) {
200: $this->_mailEncryption = $mail_encryption;
201: } elseif ('1' == $mail_encryption) {
202: $this->_mailEncryption = 'ssl';
203: } else {
204: $this->_mailEncryption = NULL;
205: }
206:
207: // get name and password of mail host user
208: $this->_mailUser = (getSystemProperty('system', 'mail_user')) ? getSystemProperty('system', 'mail_user') : '';
209: $this->_mailPass = (getSystemProperty('system', 'mail_pass')) ? getSystemProperty('system', 'mail_pass') : '';
210:
211: // build transport
212: $transport = self::constructTransport($this->_mailHost, $this->_mailPort, $this->_mailEncryption, $this->_mailUser, $this->_mailPass);
213: if($transport == false) {
214: return false;
215: }
216: parent::__construct($transport);
217: }
218:
219: /**
220: * This factory method tries to establish an SMTP connection to the
221: * given mail host. If an optional mail host user is given it is
222: * used to authenticate at the mail host. On success a SMTP transport
223: * instance is returned. On failure a simple MAIL transport instance
224: * is created and returned which will use PHP's mail() function to
225: * send mails.
226: *
227: * @todo making this a static method and passing all the params is
228: * not that smart!
229: * @param string $mailHost
230: * the mail host
231: * @param string $mailPort
232: * the mail port
233: * @param string $mailEncryption [optional]
234: * the mail encryption, none by default
235: * @param string $mailUser [optional]
236: * the mail user, none by default
237: * @param string $mailPass [optional]
238: * the mail password, none by default
239: * @return Swift_Transport
240: * the transport object
241: */
242: public static function constructTransport($mailHost, $mailPort, $mailEncryption = NULL, $mailUser = NULL, $mailPass = NULL) {
243:
244: // use SMTP by default
245: $transport = Swift_SmtpTransport::newInstance($mailHost, $mailPort, $mailEncryption);
246:
247: // use optional mail user to authenticate at mail host
248: if (!empty($mailUser)) {
249: $authHandler = new Swift_Transport_Esmtp_AuthHandler(array(
250: new Swift_Transport_Esmtp_Auth_PlainAuthenticator(),
251: new Swift_Transport_Esmtp_Auth_LoginAuthenticator(),
252: new Swift_Transport_Esmtp_Auth_CramMd5Authenticator()
253: ));
254: $authHandler->setUsername($mailUser);
255: if (!empty($mailPass)) {
256: $authHandler->setPassword($mailPass);
257: }
258: $transport->setExtensionHandlers(array(
259: $authHandler
260: ));
261: }
262:
263: // check if SMTP usage is possible
264: try {
265: $transport->start();
266: } catch (Swift_TransportException $e) {
267: // if SMTP fails just use PHP's mail() function
268: // $transport = Swift_MailTransport::newInstance();
269:
270: // CON-2540
271: // fallback in constructTransport deleted
272: // parent::send() can't handle it, therefore return null before
273: return false;
274: }
275:
276: return $transport;
277: }
278:
279: /**
280: * Sets the charset of the messages which are sent by this mailer.
281: * If you want to use UTF-8, you do not need to call this method.
282: *
283: * @param string $charset
284: * the character encoding
285: */
286: public function setCharset($charset) {
287: Swift_Preferences::getInstance()->setCharset($charset);
288: }
289:
290: /**
291: * Wrapper function for sending a mail.
292: *
293: * All parameters which accept mail addresses also accept an array
294: * where the key is the email address and the value is the name.
295: *
296: * @param string|array $from
297: * the sender of the mail, if something "empty" is given,
298: * default address from CONTENIDO system settings is used
299: * @param string|array $to
300: * one or more recipient addresses
301: * @param string $subject
302: * the subject of the mail
303: * @param string $body [optional]
304: * the body of the mail
305: * @param string|array $cc [optional]
306: * one or more recipient addresses which should get a normal copy
307: * @param string|array $bcc [optional]
308: * one or more recipient addresses which should get a blind copy
309: * @param string|array $replyTo [optional]
310: * address to which replies should be sent
311: * @param bool $resend [optional]
312: * whether the mail is resent
313: * @param string $contentType [optional]
314: * MIME type to use for mail, defaults to 'text/plain'
315: * @return int
316: * number of recipients to which the mail has been sent
317: */
318: public function sendMail($from, $to, $subject, $body = '', $cc = NULL, $bcc = NULL, $replyTo = NULL, $resend = false, $contentType = 'text/plain') {
319:
320: $message = Swift_Message::newInstance($subject, $body, $contentType);
321: if (empty($from) || is_array($from) && count($from) > 1) {
322: $message->setFrom(array(
323: $this->_mailSender => $this->_mailSenderName
324: ));
325: } else {
326: $message->setFrom($from);
327: }
328: $message->setTo($to);
329: $message->setCc($cc);
330: $message->setBcc($bcc);
331: $message->setReplyTo($replyTo);
332:
333: $failedRecipients = array();
334: return $this->send($message, $failedRecipients, $resend);
335: }
336:
337: /**
338: * Sends the given Swift_Mime_Message and logs it if $resend is false.
339: *
340: * @see Swift_Mailer::send()
341: * @param Swift_Mime_Message $message
342: * the message to send
343: * @param array &$failedRecipients [optional]
344: * list of recipients for which the sending has failed
345: * @param bool $resend [optional]
346: * if this mail is send via resend
347: * when resending a mail it is not logged again
348: * @return int
349: */
350: public function send(Swift_Mime_Message $message, &$failedRecipients = NULL, $resend = false) {
351: if (!is_array($failedRecipients)) {
352: $failedRecipients = array();
353: }
354:
355: // CON-2540
356: // fallback in constructTransport deleted
357: // parent::send() can't handle it, therefore return null before
358: if($this->getTransport() == null) {
359: return null;
360: }
361: $result = parent::send($message, $failedRecipients);
362:
363: // log the mail only if it is a new one
364: if (!$resend) {
365: $this->_logMail($message, $failedRecipients);
366: }
367:
368: return $result;
369: }
370:
371: /**
372: * Resends the mail with the given idmailsuccess.
373: *
374: * @param int $idmailsuccess
375: * ID of the mail which should be resend
376: * @throws cInvalidArgumentException
377: * if the mail has already been sent successfully or does not exist
378: */
379: public function resendMail($idmailsuccess) {
380: $mailLogSuccess = new cApiMailLogSuccess($idmailsuccess);
381: if (!$mailLogSuccess->isLoaded() || $mailLogSuccess->get('success') == 1) {
382: throw new cInvalidArgumentException('The mail which should be resent has already been sent successfully or does not exist.');
383: }
384:
385: // get all fields, json-decode address fields
386: $idmail = $mailLogSuccess->get('idmail');
387: $mailLog = new cApiMailLog($idmail);
388: $from = json_decode($mailLog->get('from'), true);
389: $to = json_decode($mailLog->get('to'), true);
390: $replyTo = json_decode($mailLog->get('reply_to'), true);
391: $cc = json_decode($mailLog->get('cc'), true);
392: $bcc = json_decode($mailLog->get('bcc'), true);
393: $subject = $mailLog->get('subject');
394: $body = $mailLog->get('body');
395: $contentType = $mailLog->get('content_type');
396: $this->setCharset($mailLog->get('charset'));
397:
398: // decode all fields
399: $charset = $mailLog->get('charset');
400: $from = $this->decodeField($from, $charset);
401: $to = $this->decodeField($to, $charset);
402: $replyTo = $this->decodeField($replyTo, $charset);
403: $cc = $this->decodeField($cc, $charset);
404: $bcc = $this->decodeField($bcc, $charset);
405: $subject = $this->decodeField($subject, $charset);
406: $body = $this->decodeField($body, $charset);
407:
408: $success = $this->sendMail($from, $to, $subject, $body, $cc, $bcc, $replyTo, true, $contentType);
409:
410: if ($success) {
411: $mailLogSuccess->set('success', 1);
412: $mailLogSuccess->store();
413: }
414: }
415:
416: /**
417: * Encodes the given value / array of values using conHtmlEntities().
418: *
419: * @todo check why conHtmlEntities() is called w/ 4 params
420: * @param string|array $value
421: * the value to encode
422: * @param string $charset
423: * the charset to use
424: * @return string|array
425: * encoded value
426: */
427: private function encodeField($value, $charset) {
428: if (is_array($value)) {
429: for ($i = 0; $i < count($value); $i++) {
430: if (!empty($value[$i])) {
431: $value[$i] = conHtmlentities($value[$i], ENT_COMPAT, $charset, false);
432: }
433: }
434: return $value;
435: } else if (is_string($value)) {
436: return conHtmlentities($value, ENT_COMPAT, $charset, false);
437: }
438: return $value;
439: }
440:
441: /**
442: * Decodes the given value / array of values using conHtmlEntityDecode().
443: *
444: * @todo check why conHtmlEntityDecode() is called w/ 4 params
445: * @param string|array $value
446: * the value to decode
447: * @param string $charset
448: * the charset to use
449: * @return string|array
450: * decoded value
451: */
452: private function decodeField($value, $charset) {
453: if (is_array($value)) {
454: for ($i = 0; $i < count($value); $i++) {
455: if (!empty($value[$i])) {
456: $value[$i] = conHtmlEntityDecode($value[$i], ENT_COMPAT | ENT_HTML401, $charset, false);
457: }
458: }
459: } else if (is_string($value)) {
460: return conHtmlEntityDecode($value, ENT_COMPAT | ENT_HTML401, $charset);
461: }
462: return $value;
463: }
464:
465: /**
466: * Log the information about sending the email.
467: *
468: * @param Swift_Message $message
469: * the message which has been send
470: * @param array $failedRecipients [optional]
471: * the recipient addresses that did not get the mail
472: * @return string|bool
473: * the idmail of the inserted table row in con_mail_log|bool
474: * false if mail_log option is inactive
475: */
476: private function _logMail(Swift_Mime_Message $message, array $failedRecipients = array()) {
477:
478: // Log only if mail_log is active otherwise return false
479: $mail_log = getSystemProperty('system', 'mail_log');
480: if ($mail_log == 'false') {
481: return false;
482: }
483:
484: $mailLogCollection = new cApiMailLogCollection();
485:
486: // encode all fields
487: $charset = $message->getCharset();
488: $from = $this->encodeField($message->getFrom(), $charset);
489: $to = $this->encodeField($message->getTo(), $charset);
490: $replyTo = $this->encodeField($message->getReplyTo(), $charset);
491: $cc = $this->encodeField($message->getCc(), $charset);
492: $bcc = $this->encodeField($message->getBcc(), $charset);
493: $subject = $this->encodeField($message->getSubject(), $charset);
494: $body = $this->encodeField($message->getBody(), $charset);
495: $contentType = $message->getContentType();
496: $mailItem = $mailLogCollection->create($from, $to, $replyTo, $cc, $bcc, $subject, $body, time(), $charset, $contentType);
497:
498: // get idmail variable
499: $idmail = $mailItem->get('idmail');
500:
501: // do not use array_merge here since the mail addresses are array keys
502: // array_merge will make problems if one recipient is e.g. in cc and bcc
503: $recipientArrays = array(
504: $message->getTo(),
505: $message->getCc(),
506: $message->getBcc()
507: );
508: $mailLogSuccessCollection = new cApiMailLogSuccessCollection();
509: foreach ($recipientArrays as $recipients) {
510: if (!is_array($recipients)) {
511: continue;
512: }
513: foreach ($recipients as $key => $value) {
514: $recipient = array(
515: $key => $value
516: );
517: $success = true;
518: // TODO how do we get the information why message sending
519: // has failed?
520: $exception = '';
521: if (in_array($key, $failedRecipients)) {
522: $success = false;
523: }
524: $mailLogSuccessCollection->create($idmail, $recipient, $success, $exception);
525: }
526: }
527:
528: return $idmail;
529: }
530:
531: }
532: