1: <?php
2:
3: namespace Charcoal\Email;
4:
5:
6: use \Exception;
7: use \InvalidArgumentException;
8:
9:
10: use \Psr\Log\LoggerAwareInterface;
11: use \Psr\Log\LoggerAwareTrait;
12:
13:
14: use \PHPMailer\PHPMailer\PHPMailer;
15:
16:
17: use \Charcoal\Config\ConfigurableInterface;
18: use \Charcoal\Config\ConfigurableTrait;
19:
20:
21: use \Charcoal\Factory\FactoryInterface;
22:
23:
24: use \Charcoal\View\GenericView;
25: use \Charcoal\View\ViewableInterface;
26: use \Charcoal\View\ViewableTrait;
27:
28:
29: use \Charcoal\Queue\QueueableInterface;
30: use \Charcoal\Queue\QueueableTrait;
31:
32:
33: use \Charcoal\Email\EmailInterface;
34: use \Charcoal\Email\EmailConfig;
35: use \Charcoal\Email\EmailLog;
36:
37: 38: 39:
40: class Email implements
41: ConfigurableInterface,
42: EmailInterface,
43: LoggerAwareInterface,
44: QueueableInterface,
45: ViewableInterface
46: {
47: use ConfigurableTrait;
48: use LoggerAwareTrait;
49: use QueueableTrait;
50: use ViewableTrait;
51: use EmailAwareTrait;
52:
53: 54: 55: 56: 57:
58: private $campaign;
59:
60: 61: 62: 63: 64:
65: private $to = [];
66:
67: 68: 69: 70: 71:
72: private $cc = [];
73:
74: 75: 76: 77: 78:
79: private $bcc = [];
80:
81: 82: 83: 84: 85:
86: private $from;
87:
88: 89: 90: 91: 92:
93: private $replyTo;
94:
95: 96: 97: 98: 99:
100: private $subject;
101:
102: 103: 104: 105: 106:
107: private $msgHtml;
108:
109: 110: 111: 112: 113:
114: private $msgTxt;
115:
116: 117: 118:
119: private $attachments = [];
120:
121: 122: 123: 124: 125:
126: private $log;
127:
128: 129: 130: 131: 132:
133: private $track;
134:
135: 136: 137: 138: 139:
140: private $templateData = [];
141:
142: 143: 144:
145: private $phpMailer;
146:
147: 148: 149:
150: private $templateFactory;
151:
152: 153: 154:
155: private $queueItemFactory;
156:
157: 158: 159:
160: private $logFactory;
161:
162: 163: 164: 165: 166:
167: public function __construct(array $data)
168: {
169: $this->phpMailer = new PHPMailer(true);
170: $this->setLogger($data['logger']);
171: $this->setView($data['view']);
172: $this->setConfig($data['config']);
173: $this->setTemplateFactory($data['template_factory']);
174: $this->setQueueItemFactory($data['queue_item_factory']);
175: $this->setLogFactory($data['log_factory']);
176: }
177:
178: 179: 180: 181:
182: protected function setTemplateFactory(FactoryInterface $factory)
183: {
184: $this->templateFactory = $factory;
185: return $this;
186: }
187:
188: 189: 190:
191: protected function templateFactory()
192: {
193: return $this->templateFactory;
194: }
195:
196: 197: 198: 199:
200: protected function setQueueItemFactory(FactoryInterface $factory)
201: {
202: $this->queueItemFactory = $factory;
203: return $this;
204: }
205:
206: 207: 208:
209: protected function queueItemFactory()
210: {
211: return $this->queueItemFactory;
212: }
213:
214: 215: 216: 217:
218: protected function setLogFactory(FactoryInterface $factory)
219: {
220: $this->logFactory = $factory;
221: return $this;
222: }
223:
224: 225: 226:
227: protected function logFactory()
228: {
229: return $this->logFactory;
230: }
231:
232: 233: 234: 235: 236: 237:
238: public function setData(array $data)
239: {
240: foreach ($data as $prop => $val) {
241: $func = [$this, $this->setter($prop)];
242: if (is_callable($func)) {
243: call_user_func($func, $val);
244: } else {
245: $this->{$prop} = $val;
246: }
247: }
248:
249: return $this;
250: }
251:
252: 253: 254: 255: 256: 257: 258:
259: public function setCampaign($campaign)
260: {
261: if (!is_string($campaign)) {
262: throw new InvalidArgumentException(
263: 'Campaign must be a string'
264: );
265: }
266:
267: $this->campaign = $campaign;
268:
269: return $this;
270: }
271:
272: 273: 274: 275: 276: 277: 278:
279: public function campaign()
280: {
281: if ($this->campaign === null) {
282: $this->campaign = $this->generateCampaign();
283: }
284:
285: return $this->campaign;
286: }
287:
288: 289: 290: 291: 292:
293: protected function generateCampaign()
294: {
295: return uniqid();
296: }
297:
298: 299: 300: 301: 302: 303: 304:
305: public function setTo($email)
306: {
307: if (is_string($email)) {
308: $email = [ $email ];
309: }
310:
311: if (!is_array($email)) {
312: throw new InvalidArgumentException(
313: 'Must be an array of recipients.'
314: );
315: }
316:
317: $this->to = [];
318:
319:
320: if (isset($email['email'])) {
321:
322: $this->addTo($email);
323: } else {
324: foreach ($email as $recipient) {
325: $this->addTo($recipient);
326: }
327: }
328:
329: return $this;
330: }
331:
332: 333: 334: 335: 336: 337: 338:
339: public function addTo($email)
340: {
341: if (is_string($email)) {
342: $this->to[] = $email;
343: } elseif (is_array($email)) {
344: $this->to[] = $this->emailFromArray($email);
345: } else {
346: throw new InvalidArgumentException(
347: 'Can not set to: email must be an array or a string'
348: );
349: }
350:
351: return $this;
352: }
353:
354: 355: 356: 357: 358:
359: public function to()
360: {
361: return $this->to;
362: }
363:
364: 365: 366: 367: 368: 369: 370:
371: public function setCc($email)
372: {
373: if (is_string($email)) {
374: $email = [ $email ];
375: }
376:
377: if (!is_array($email)) {
378: throw new InvalidArgumentException(
379: 'Must be an array of CC recipients.'
380: );
381: }
382:
383: $this->cc = [];
384:
385:
386: if (isset($email['email'])) {
387:
388: $this->addCc($email);
389: } else {
390: foreach ($email as $recipient) {
391: $this->addCc($recipient);
392: }
393: }
394:
395: return $this;
396: }
397:
398: 399: 400: 401: 402: 403: 404:
405: public function addCc($email)
406: {
407: if (is_string($email)) {
408: $this->cc[] = $email;
409: } elseif (is_array($email)) {
410: $this->cc[] = $this->emailFromArray($email);
411: } else {
412: throw new InvalidArgumentException(
413: 'Can not set to: email must be an array or a string'
414: );
415: }
416:
417: return $this;
418: }
419:
420: 421: 422: 423: 424:
425: public function cc()
426: {
427: return $this->cc;
428: }
429:
430: 431: 432: 433: 434: 435: 436:
437: public function setBcc($email)
438: {
439: if (is_string($email)) {
440:
441: $email = [ $email ];
442: }
443:
444: if (!is_array($email)) {
445: throw new InvalidArgumentException(
446: 'Must be an array of BCC recipients.'
447: );
448: }
449:
450: $this->bcc = [];
451:
452:
453: if (isset($email['email'])) {
454:
455: $this->addBcc($email);
456: } else {
457: foreach ($email as $recipient) {
458: $this->addBcc($recipient);
459: }
460: }
461:
462: return $this;
463: }
464:
465: 466: 467: 468: 469: 470: 471:
472: public function addBcc($email)
473: {
474: if (is_string($email)) {
475: $this->bcc[] = $email;
476: } elseif (is_array($email)) {
477: $this->bcc[] = $this->emailFromArray($email);
478: } else {
479: throw new InvalidArgumentException(
480: 'Can not set to: email must be an array or a string'
481: );
482: }
483:
484: return $this;
485: }
486:
487: 488: 489: 490: 491:
492: public function bcc()
493: {
494: return $this->bcc;
495: }
496:
497: 498: 499: 500: 501: 502: 503: 504:
505: public function setFrom($email)
506: {
507: if (is_array($email)) {
508: $this->from = $this->emailFromArray($email);
509: } elseif (is_string($email)) {
510: $this->from = $email;
511: } else {
512: throw new InvalidArgumentException(
513: 'Can not set from: email must be an array or a string'
514: );
515: }
516:
517: return $this;
518: }
519:
520: 521: 522: 523: 524:
525: public function from()
526: {
527: if ($this->from === null) {
528: $this->setFrom($this->config()->defaultFrom());
529: }
530:
531: return $this->from;
532: }
533:
534: 535: 536: 537: 538: 539: 540:
541: public function setReplyTo($email)
542: {
543: if (is_array($email)) {
544: $this->replyTo = $this->emailFromArray($email);
545: } elseif (is_string($email)) {
546: $this->replyTo = $email;
547: } else {
548: throw new InvalidArgumentException(
549: 'Can not set reply-to: email must be an array or a string'
550: );
551: }
552:
553: return $this;
554: }
555:
556: 557: 558: 559: 560:
561: public function replyTo()
562: {
563: if ($this->replyTo === null) {
564: $this->replyTo = $this->config()->defaultReplyTo();
565: }
566:
567: return $this->replyTo;
568: }
569:
570: 571: 572: 573: 574: 575: 576:
577: public function setSubject($subject)
578: {
579: if (!is_string($subject)) {
580: throw new InvalidArgumentException(
581: 'Subject needs to be a string'
582: );
583: }
584:
585: $this->subject = $subject;
586:
587: return $this;
588: }
589:
590: 591: 592: 593: 594:
595: public function subject()
596: {
597: return $this->subject;
598: }
599:
600: 601: 602: 603: 604: 605: 606:
607: public function setMsgHtml($body)
608: {
609: if (!is_string($body)) {
610: throw new InvalidArgumentException(
611: 'HTML message needs to be a string'
612: );
613: }
614:
615: $this->msgHtml = $body;
616:
617: return $this;
618: }
619:
620: 621: 622: 623: 624: 625: 626: 627:
628: public function msgHtml()
629: {
630: if ($this->msgHtml === null) {
631: $this->msgHtml = $this->generateMsgHtml();
632: }
633: return $this->msgHtml;
634: }
635:
636: 637: 638: 639: 640: 641:
642: protected function generateMsgHtml()
643: {
644: $templateIdent = $this->templateIdent();
645:
646: if (!$templateIdent) {
647: $message = '';
648: } else {
649: $message = $this->renderTemplate($templateIdent);
650: }
651:
652: return $message;
653: }
654:
655: 656: 657: 658: 659: 660: 661:
662: public function setMsgTxt($body)
663: {
664: if (!is_string($body)) {
665: throw new InvalidArgumentException(
666: 'Plan-text message needs to be a string'
667: );
668: }
669:
670: $this->msgTxt = $body;
671:
672: return $this;
673: }
674:
675: 676: 677: 678: 679: 680: 681: 682:
683: public function msgTxt()
684: {
685: if ($this->msgTxt === null) {
686: $this->msgTxt = $this->stripHtml($this->msgHtml());
687: }
688:
689: return $this->msgTxt;
690: }
691:
692: 693: 694: 695: 696: 697:
698: protected function stripHtml($html)
699: {
700: $str = html_entity_decode($html);
701:
702:
703: $str = preg_replace('#<br[^>]*?>#siu', "\n", $str);
704: $str = preg_replace(
705: [
706: '#<applet[^>]*?.*?</applet>#siu',
707: '#<embed[^>]*?.*?</embed>#siu',
708: '#<head[^>]*?>.*?</head>#siu',
709: '#<noframes[^>]*?.*?</noframes>#siu',
710: '#<noscript[^>]*?.*?</noscript>#siu',
711: '#<noembed[^>]*?.*?</noembed>#siu',
712: '#<object[^>]*?.*?</object>#siu',
713: '#<script[^>]*?.*?</script>#siu',
714: '#<style[^>]*?>.*?</style>#siu'
715: ],
716: '',
717: $str
718: );
719: $str = strip_tags($str);
720:
721:
722: $str = str_replace("\t", '', $str);
723: $str = preg_replace('#\n\r|\r\n#', "\n", $str);
724: $str = preg_replace('#\n{3,}#', "\n\n", $str);
725: $str = preg_replace('/ {2,}/', ' ', $str);
726: $str = implode("\n", array_map('trim', explode("\n", $str)));
727: $str = trim($str)."\n";
728: return $str;
729: }
730:
731: 732: 733: 734: 735: 736:
737: public function setAttachments(array $attachments)
738: {
739: foreach ($attachments as $att) {
740: $this->addAttachment($att);
741: }
742:
743: return $this;
744: }
745:
746: 747: 748: 749: 750: 751:
752: public function addAttachment($attachment)
753: {
754: $this->attachments[] = $attachment;
755: return $this;
756: }
757:
758: 759: 760: 761: 762:
763: public function attachments()
764: {
765: return $this->attachments;
766: }
767:
768: 769: 770: 771: 772: 773:
774: public function setLog($log)
775: {
776: $this->log = !!$log;
777: return $this;
778: }
779:
780: 781: 782: 783: 784:
785: public function log()
786: {
787: if ($this->log === null) {
788: $this->log = $this->config()->defaultLog();
789: }
790: return $this->log;
791: }
792:
793: 794: 795: 796: 797: 798:
799: public function setTrack($track)
800: {
801: $this->track = !!$track;
802: return $this;
803: }
804:
805: 806: 807: 808: 809:
810: public function track()
811: {
812: if ($this->track === null) {
813: $this->track = $this->config()->defaultTrack();
814: }
815: return $this->track;
816: }
817:
818: 819: 820: 821: 822: 823: 824:
825: public function send()
826: {
827: $this->logger->debug(
828: 'Attempting to send an email',
829: $this->to()
830: );
831:
832: $mail = $this->phpMailer;
833:
834: try {
835: $this->setSmtpOptions($mail);
836:
837: $mail->CharSet = 'UTF-8';
838:
839:
840: $replyTo = $this->replyTo();
841: if ($replyTo) {
842: $replyArr = $this->emailToArray($replyTo);
843: $mail->addReplyTo($replyArr['email'], $replyArr['name']);
844: }
845:
846:
847: $from = $this->from();
848: $fromArr = $this->emailToArray($from);
849: $mail->setFrom($fromArr['email'], $fromArr['name']);
850:
851:
852: $to = $this->to();
853: foreach ($to as $recipient) {
854: $toArr = $this->emailToArray($recipient);
855: $mail->addAddress($toArr['email'], $toArr['name']);
856: }
857:
858:
859: $cc = $this->cc();
860: foreach ($cc as $ccRecipient) {
861: $ccArr = $this->emailToArray($ccRecipient);
862: $mail->addCC($ccArr['email'], $ccArr['name']);
863: }
864:
865:
866: $bcc = $this->bcc();
867: foreach ($bcc as $bccRecipient) {
868: $bccArr = $this->emailToArray($bccRecipient);
869: $mail->addBCC($bccArr['email'], $cc['name']);
870: }
871:
872:
873: $attachments = $this->attachments();
874: foreach ($attachments as $att) {
875: $mail->addAttachment($att);
876: }
877:
878: $mail->isHTML(true);
879:
880: $mail->Subject = $this->subject();
881: $mail->Body = $this->msgHtml();
882: $mail->AltBody = $this->msgTxt();
883:
884: $ret = $mail->send();
885:
886: $this->logSend($ret, $mail);
887:
888: return $ret;
889: } catch (Exception $e) {
890: $this->logger->error(
891: sprintf('Error sending email: %s', $e->getMessage())
892: );
893: }
894: }
895:
896: 897: 898: 899: 900: 901:
902: public function setSmtpOptions(PHPMailer $mail)
903: {
904: $config = $this->config();
905: if (!$config['smtp']) {
906: return;
907: }
908:
909: $this->logger->debug(
910: sprintf('Using SMTP "%s" server to send email', $config['smtp_hostname'])
911: );
912:
913: $mail->IsSMTP();
914: $mail->Host = $config['smtp_hostname'];
915: $mail->Port = $config['smtp_port'];
916: $mail->SMTPAuth = $config['smtp_auth'];
917: $mail->Username = $config['smtp_username'];
918: $mail->Password = $config['smtp_password'];
919: $mail->SMTPSecure = $config['smtp_security'];
920: }
921:
922: 923: 924: 925: 926: 927:
928: public function queue($ts = null)
929: {
930: $recipients = $this->to();
931: $author = $this->from();
932: $subject = $this->subject();
933: $msgHtml = $this->msgHtml();
934: $msgTxt = $this->msgTxt();
935: $campaign = $this->campaign();
936: $queueId = $this->queueId();
937:
938: foreach ($recipients as $to) {
939: $queueItem = $this->queueItemFactory()->create('charcoal/email/email-queue-item');
940:
941: $queueItem->setTo($to);
942: $queueItem->setFrom($author);
943: $queueItem->setSubject($subject);
944: $queueItem->setMsgHtml($msgHtml);
945: $queueItem->setMsgTxt($msgTxt);
946: $queueItem->setCampaign($campaign);
947: $queueItem->setProcessingDate($ts);
948: $queueItem->setQueueId($queueId);
949:
950: $res = $queueItem->save();
951: }
952:
953: return true;
954: }
955:
956: 957: 958: 959: 960: 961: 962:
963: protected function logSend($result, $mailer)
964: {
965: if ($this->log() === false) {
966: return;
967: }
968:
969: if (!$result) {
970: $this->logger->error('Email could not be sent.');
971: } else {
972: $this->logger->debug(
973: sprintf('Email "%s" sent successfully.', $this->subject()),
974: $this->to()
975: );
976: }
977:
978: $recipients = array_merge(
979: $this->to(),
980: $this->cc(),
981: $this->bcc()
982: );
983:
984: foreach ($recipients as $to) {
985: $log = $this->logFactory()->create('charcoal/email/email-log');
986:
987: $log->setType('email');
988: $log->setAction('send');
989:
990: $log->setRawResponse($mailer);
991:
992: $log->setMessageId($mailer->getLastMessageId());
993: $log->setCampaign($this->campaign());
994:
995: $log->setSendDate('now');
996:
997: $log->setFrom($mailer->From);
998: $log->setTo($to);
999: $log->setSubject($this->subject());
1000:
1001: $log->save();
1002: }
1003: }
1004:
1005: 1006: 1007: 1008: 1009: 1010:
1011: protected function logQueue()
1012: {
1013: }
1014:
1015: 1016: 1017: 1018: 1019: 1020:
1021: public function setTemplateData(array $data)
1022: {
1023: $this->templateData = $data;
1024: return $this;
1025: }
1026:
1027: 1028: 1029: 1030: 1031:
1032: public function templateData()
1033: {
1034: return $this->templateData;
1035: }
1036:
1037: 1038: 1039: 1040: 1041: 1042: 1043: 1044: 1045: 1046:
1047: public function viewController()
1048: {
1049: $templateIdent = $this->templateIdent();
1050:
1051: if (!$templateIdent) {
1052: return [];
1053: }
1054:
1055: $templateFactory = clone($this->templateFactory());
1056: $templateFactory->setDefaultClass('charcoal/email/generic-email');
1057: $template = $templateFactory->create($templateIdent);
1058:
1059: $template->setData($this->templateData());
1060:
1061: return $template;
1062: }
1063:
1064: 1065: 1066: 1067: 1068: 1069:
1070: protected function getter($key)
1071: {
1072: $getter = $key;
1073: return $this->camelize($getter);
1074: }
1075:
1076: 1077: 1078: 1079: 1080: 1081:
1082: protected function setter($key)
1083: {
1084: $setter = 'set_'.$key;
1085: return $this->camelize($setter);
1086: }
1087:
1088: 1089: 1090: 1091: 1092: 1093:
1094: private function camelize($str)
1095: {
1096: return lcfirst(implode('', array_map('ucfirst', explode('_', $str))));
1097: }
1098:
1099: 1100: 1101: 1102: 1103:
1104: public function createConfig()
1105: {
1106:
1107: $this->logger->warning('AbstractEmail::createConfig() was called, but should not.');
1108: return new \Charcoal\Email\EmailConfig();
1109: }
1110: }
1111: