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: *
158: * @param Swift_Transport $transport [optional]
159: * a transport instance
160: *
161: * @throws cDbException
162: * @throws cException
163: * @throws cInvalidArgumentException
164: * @throws Swift_DependencyException
165: * @throws Swift_RfcComplianceException
166: */
167: public function __construct($transport = NULL) {
168:
169: // get address of default mail sender
170: $mailSender = getSystemProperty('system', 'mail_sender');
171: if (Swift_Validate::email($mailSender)) {
172: $this->_mailSender = $mailSender;
173: }
174:
175: // get name of default mail sender
176: $mailSenderName = getSystemProperty('system', 'mail_sender_name');
177: if (!empty($mailSenderName)) {
178: $this->_mailSenderName = $mailSenderName;
179: }
180:
181: // if a transport object has been given, use it and skip the rest
182: if (!is_null($transport)) {
183: parent::__construct($transport);
184: return;
185: }
186: // if no transport object has been given, read system setting and create one
187:
188: // get name of mail host
189: $mailHost = getSystemProperty('system', 'mail_host');
190: if (!empty($mailHost)) {
191: $this->_mailHost = $mailHost;
192: }
193:
194: // get port of mail host
195: if (is_numeric(getSystemProperty('system', 'mail_port'))) {
196: $this->_mailPort = (int) getSystemProperty('system', 'mail_port');
197: }
198:
199: // get mail encryption
200: $encryptions = array(
201: 'tls',
202: 'ssl'
203: );
204:
205: $mail_type = cString::toLowerCase(getSystemProperty('system', 'mail_transport'));
206:
207: if ($mail_type == 'smtp') {
208:
209: $mail_encryption = cString::toLowerCase(getSystemProperty('system', 'mail_encryption'));
210: if (in_array($mail_encryption, $encryptions)) {
211: $this->_mailEncryption = $mail_encryption;
212: } elseif ('1' == $mail_encryption) {
213: $this->_mailEncryption = 'ssl';
214: } else {
215: $this->_mailEncryption = NULL;
216: }
217:
218: // get name and password of mail host user
219: $this->_mailUser = (getSystemProperty('system', 'mail_user')) ? getSystemProperty('system', 'mail_user') : '';
220: $this->_mailPass = (getSystemProperty('system', 'mail_pass')) ? getSystemProperty('system', 'mail_pass') : '';
221:
222: // build transport
223: $transport = self::constructTransport($this->_mailHost, $this->_mailPort, $this->_mailEncryption, $this->_mailUser, $this->_mailPass);
224:
225: } else {
226: $transport = Swift_MailTransport::newInstance();
227: }
228:
229: // CON-2530
230: if ($transport === false) {
231: throw new cInvalidArgumentException('Can not connect to the mail server. Please check your mail server configuration at CONTENIDO backend.');
232: }
233:
234: parent::__construct($transport);
235: }
236:
237: /**
238: * This factory method tries to establish an SMTP connection to the
239: * given mail host. If an optional mail host user is given it is
240: * used to authenticate at the mail host. On success a SMTP transport
241: * instance is returned. On failure a simple MAIL transport instance
242: * is created and returned which will use PHP's mail() function to
243: * send mails.
244: *
245: * @todo making this a static method and passing all the params is
246: * not that smart!
247: * @param string $mailHost
248: * the mail host
249: * @param string $mailPort
250: * the mail port
251: * @param string $mailEncryption [optional]
252: * the mail encryption, none by default
253: * @param string $mailUser [optional]
254: * the mail user, none by default
255: * @param string $mailPass [optional]
256: * the mail password, none by default
257: * @return Swift_Transport
258: * the transport object
259: */
260: public static function constructTransport($mailHost, $mailPort, $mailEncryption = NULL, $mailUser = NULL, $mailPass = NULL) {
261:
262: // use SMTP by default
263: $transport = Swift_SmtpTransport::newInstance($mailHost, $mailPort, $mailEncryption);
264:
265: // use optional mail user to authenticate at mail host
266: if (!empty($mailUser)) {
267: $authHandler = new Swift_Transport_Esmtp_AuthHandler(array(
268: new Swift_Transport_Esmtp_Auth_PlainAuthenticator(),
269: new Swift_Transport_Esmtp_Auth_LoginAuthenticator(),
270: new Swift_Transport_Esmtp_Auth_CramMd5Authenticator()
271: ));
272: $authHandler->setUsername($mailUser);
273: if (!empty($mailPass)) {
274: $authHandler->setPassword($mailPass);
275: }
276: $transport->setExtensionHandlers(array(
277: $authHandler
278: ));
279: }
280:
281: // check if SMTP usage is possible
282: try {
283: $transport->start();
284: } catch (Swift_TransportException $e) {
285: // CON-2530
286: // fallback in constructTransport deleted
287: // parent::send() can't handle it, therefore return false before
288: return false;
289: }
290:
291: return $transport;
292: }
293:
294: /**
295: * Sets the charset of the messages which are sent by this mailer.
296: * If you want to use UTF-8, you do not need to call this method.
297: *
298: * @param string $charset
299: * the character encoding
300: */
301: public function setCharset($charset) {
302: Swift_Preferences::getInstance()->setCharset($charset);
303: }
304:
305: /**
306: * Wrapper function for sending a mail.
307: *
308: * All parameters which accept mail addresses also accept an array
309: * where the key is the email address and the value is the name.
310: *
311: * @param string|array $from
312: * the sender of the mail, if something "empty" is given,
313: * default address from CONTENIDO system settings is used
314: * @param string|array $to
315: * one or more recipient addresses
316: * @param string $subject
317: * the subject of the mail
318: * @param string $body [optional]
319: * the body of the mail
320: * @param string|array $cc [optional]
321: * one or more recipient addresses which should get a normal copy
322: * @param string|array $bcc [optional]
323: * one or more recipient addresses which should get a blind copy
324: * @param string|array $replyTo [optional]
325: * address to which replies should be sent
326: * @param bool $resend [optional]
327: * whether the mail is resent
328: * @param string $contentType [optional]
329: * MIME type to use for mail, defaults to 'text/plain'
330: *
331: * @return int
332: * number of recipients to which the mail has been sent
333: *
334: * @throws cDbException
335: * @throws cException
336: * @throws cInvalidArgumentException
337: */
338: public function sendMail($from, $to, $subject, $body = '', $cc = NULL, $bcc = NULL, $replyTo = NULL, $resend = false, $contentType = 'text/plain') {
339:
340: $message = Swift_Message::newInstance($subject, $body, $contentType);
341: if (empty($from) || is_array($from) && count($from) > 1) {
342: $message->setFrom(array(
343: $this->_mailSender => $this->_mailSenderName
344: ));
345: } else {
346: $message->setFrom($from);
347: }
348: $message->setTo($to);
349: $message->setCc($cc);
350: $message->setBcc($bcc);
351: $message->setReplyTo($replyTo);
352:
353: $failedRecipients = array();
354: return $this->send($message, $failedRecipients, $resend);
355: }
356:
357: /**
358: * Sends the given Swift_Mime_Message and logs it if $resend is false.
359: *
360: * @see Swift_Mailer::send()
361: *
362: * @param Swift_Mime_Message $message
363: * the message to send
364: * @param array &$failedRecipients [optional]
365: * list of recipients for which the sending has failed
366: * @param bool $resend [optional]
367: * if this mail is send via resend
368: * when resending a mail it is not logged again
369: *
370: * @return int
371: *
372: * @throws cDbException
373: * @throws cException
374: * @throws cInvalidArgumentException
375: */
376: public function send(Swift_Mime_Message $message, &$failedRecipients = NULL, $resend = false) {
377: if (!is_array($failedRecipients)) {
378: $failedRecipients = array();
379: }
380:
381: // CON-2540
382: // fallback in constructTransport deleted
383: // parent::send() can't handle it, therefore return null before
384: if($this->getTransport() == null) {
385: return null;
386: }
387: $result = parent::send($message, $failedRecipients);
388:
389: // log the mail only if it is a new one
390: if (!$resend) {
391: $this->_logMail($message, $failedRecipients);
392: }
393:
394: return $result;
395: }
396:
397: /**
398: * Resends the mail with the given idmailsuccess.
399: *
400: * @param int $idmailsuccess
401: * ID of the mail which should be resend
402: *
403: * @throws cDbException
404: * @throws cException
405: * @throws cInvalidArgumentException if the mail has already been sent successfully or does not exist
406: */
407: public function resendMail($idmailsuccess) {
408: $mailLogSuccess = new cApiMailLogSuccess($idmailsuccess);
409: if (!$mailLogSuccess->isLoaded() || $mailLogSuccess->get('success') == 1) {
410: throw new cInvalidArgumentException('The mail which should be resent has already been sent successfully or does not exist.');
411: }
412:
413: // get all fields, json-decode address fields
414: $idmail = $mailLogSuccess->get('idmail');
415: $mailLog = new cApiMailLog($idmail);
416: $from = json_decode($mailLog->get('from'), true);
417: $to = json_decode($mailLog->get('to'), true);
418: $replyTo = json_decode($mailLog->get('reply_to'), true);
419: $cc = json_decode($mailLog->get('cc'), true);
420: $bcc = json_decode($mailLog->get('bcc'), true);
421: $subject = $mailLog->get('subject');
422: $body = $mailLog->get('body');
423: $contentType = $mailLog->get('content_type');
424: $this->setCharset($mailLog->get('charset'));
425:
426: // decode all fields
427: $charset = $mailLog->get('charset');
428: $from = $this->decodeField($from, $charset);
429: $to = $this->decodeField($to, $charset);
430: $replyTo = $this->decodeField($replyTo, $charset);
431: $cc = $this->decodeField($cc, $charset);
432: $bcc = $this->decodeField($bcc, $charset);
433: $subject = $this->decodeField($subject, $charset);
434: $body = $this->decodeField($body, $charset);
435:
436: $success = $this->sendMail($from, $to, $subject, $body, $cc, $bcc, $replyTo, true, $contentType);
437:
438: if ($success) {
439: $mailLogSuccess->set('success', 1);
440: $mailLogSuccess->store();
441: }
442: }
443:
444: /**
445: * Encodes the given value / array of values using conHtmlEntities().
446: *
447: * @todo check why conHtmlEntities() is called w/ 4 params
448: * @param string|array $value
449: * the value to encode
450: * @param string $charset
451: * the charset to use
452: * @return string|array
453: * encoded value
454: */
455: private function encodeField($value, $charset) {
456: if (is_array($value)) {
457: for ($i = 0; $i < count($value); $i++) {
458: if (!empty($value[$i])) {
459: $value[$i] = conHtmlentities($value[$i], ENT_COMPAT, $charset);
460: }
461: }
462: return $value;
463: } else if (is_string($value)) {
464: return conHtmlentities($value, ENT_COMPAT, $charset);
465: } else {
466: return $value;
467: }
468: }
469:
470: /**
471: * Decodes the given value / array of values using conHtmlEntityDecode().
472: *
473: * @todo check why conHtmlEntityDecode() is called w/ 4 params
474: * @param string|array $value
475: * the value to decode
476: * @param string $charset
477: * the charset to use
478: * @return string|array
479: * decoded value
480: */
481: private function decodeField($value, $charset) {
482: if (is_array($value)) {
483: for ($i = 0; $i < count($value); $i++) {
484: if (!empty($value[$i])) {
485: $value[$i] = conHtmlEntityDecode($value[$i], ENT_COMPAT | ENT_HTML401, $charset, false);
486: }
487: }
488: return $value;
489: } else if (is_string($value)) {
490: return conHtmlEntityDecode($value, ENT_COMPAT | ENT_HTML401, $charset);
491: } else {
492: return $value;
493: }
494: }
495:
496: /**
497: * Log the information about sending the email.
498: *
499: * @param Swift_Mime_Message $message
500: * the message which has been send
501: * @param array $failedRecipients [optional]
502: * the recipient addresses that did not get the mail
503: *
504: * @return string|bool
505: * the idmail of the inserted table row in con_mail_log|bool
506: * false if mail_log option is inactive
507: *
508: * @throws cDbException
509: * @throws cException
510: * @throws cInvalidArgumentException
511: */
512: private function _logMail(Swift_Mime_Message $message, array $failedRecipients = array()) {
513:
514: // Log only if mail_log is active otherwise return false
515: $mail_log = getSystemProperty('system', 'mail_log');
516: if ($mail_log == 'false') {
517: return false;
518: }
519:
520: $mailLogCollection = new cApiMailLogCollection();
521:
522: // encode all fields
523: $charset = $message->getCharset();
524: $from = $this->encodeField($message->getFrom(), $charset);
525: $to = $this->encodeField($message->getTo(), $charset);
526: $replyTo = $this->encodeField($message->getReplyTo(), $charset);
527: $cc = $this->encodeField($message->getCc(), $charset);
528: $bcc = $this->encodeField($message->getBcc(), $charset);
529: $subject = $this->encodeField($message->getSubject(), $charset);
530: $body = $this->encodeField($message->getBody(), $charset);
531: $contentType = $message->getContentType();
532: $mailItem = $mailLogCollection->create($from, $to, $replyTo, $cc, $bcc, $subject, $body, time(), $charset, $contentType);
533:
534: // get idmail variable
535: $idmail = $mailItem->get('idmail');
536:
537: // do not use array_merge here since the mail addresses are array keys
538: // array_merge will make problems if one recipient is e.g. in cc and bcc
539: $recipientArrays = array(
540: $message->getTo(),
541: $message->getCc(),
542: $message->getBcc()
543: );
544: $mailLogSuccessCollection = new cApiMailLogSuccessCollection();
545: foreach ($recipientArrays as $recipients) {
546: if (!is_array($recipients)) {
547: continue;
548: }
549: foreach ($recipients as $key => $value) {
550: $recipient = array(
551: $key => $value
552: );
553: $success = true;
554: // TODO how do we get the information why message sending
555: // has failed?
556: $exception = '';
557: if (in_array($key, $failedRecipients)) {
558: $success = false;
559: }
560: $mailLogSuccessCollection->create($idmail, $recipient, $success, $exception);
561: }
562: }
563:
564: return $idmail;
565: }
566:
567: }
568: