1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
13:
14: namespace Jyxo\Mail;
15:
16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26:
27: class Sender
28: {
29: 30: 31: 32: 33:
34: const MODE_MAIL = 'mail';
35:
36: 37: 38: 39: 40:
41: const MODE_SMTP = 'smtp';
42:
43: 44: 45: 46: 47: 48:
49: const MODE_NONE = 'none';
50:
51: 52: 53: 54: 55:
56: const LINE_LENGTH = 74;
57:
58: 59: 60: 61: 62:
63: const LINE_END = "\n";
64:
65: 66: 67: 68: 69:
70: const TYPE_SIMPLE = 'simple';
71:
72: 73: 74: 75: 76:
77: const TYPE_ALTERNATIVE = 'alternative';
78:
79: 80: 81: 82: 83:
84: const TYPE_ALTERNATIVE_ATTACHMENTS = 'alternative_attachments';
85:
86: 87: 88: 89: 90:
91: const TYPE_ATTACHMENTS = 'attachments';
92:
93: 94: 95: 96: 97:
98: private $charset = 'iso-8859-2';
99:
100: 101: 102: 103: 104:
105: private $hostname = '';
106:
107: 108: 109: 110: 111:
112: private $xmailer = '';
113:
114: 115: 116: 117: 118:
119: private $encoding = Encoding::QUOTED_PRINTABLE;
120:
121: 122: 123: 124: 125:
126: private $email = null;
127:
128: 129: 130: 131: 132:
133: private $smtpHost = 'localhost';
134:
135: 136: 137: 138: 139:
140: private $smtpPort = 25;
141:
142: 143: 144: 145: 146:
147: private $smtpHelo = '';
148:
149: 150: 151: 152: 153:
154: private $smtpUser = '';
155:
156: 157: 158: 159: 160:
161: private $smtpPsw = '';
162:
163: 164: 165: 166: 167:
168: private $smtpTimeout = 5;
169:
170: 171: 172: 173: 174:
175: private $result = null;
176:
177: 178: 179: 180: 181:
182: private $boundary = array();
183:
184: 185: 186: 187: 188:
189: private $mode = self::MODE_MAIL;
190:
191: 192: 193: 194: 195:
196: private $type = self::TYPE_SIMPLE;
197:
198: 199: 200: 201: 202:
203: private $createdHeader = array();
204:
205: 206: 207: 208: 209:
210: private $createdBody = '';
211:
212: 213: 214: 215: 216:
217: public function getCharset()
218: {
219: return $this->charset;
220: }
221:
222: 223: 224: 225: 226: 227:
228: public function setCharset($charset)
229: {
230: $this->charset = (string) $charset;
231:
232: return $this;
233: }
234:
235: 236: 237: 238: 239:
240: public function getHostname()
241: {
242: if (empty($this->hostname)) {
243: $this->hostname = isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost';
244: }
245:
246: return $this->hostname;
247: }
248:
249: 250: 251: 252: 253: 254:
255: public function setHostname($hostname)
256: {
257: $this->hostname = (string) $hostname;
258:
259: return $this;
260: }
261:
262: 263: 264: 265: 266:
267: public function getXmailer()
268: {
269: return $this->xmailer;
270: }
271:
272: 273: 274: 275: 276: 277:
278: public function setXmailer($xmailer)
279: {
280: $this->xmailer = (string) $xmailer;
281:
282: return $this;
283: }
284:
285: 286: 287: 288: 289:
290: public function getEncoding()
291: {
292: return $this->encoding;
293: }
294:
295: 296: 297: 298: 299: 300: 301:
302: public function setEncoding($encoding)
303: {
304: if (!Encoding::isCompatible($encoding)) {
305: throw new \InvalidArgumentException(sprintf('Incompatible encoding %s.', $encoding));
306: }
307:
308: $this->encoding = (string) $encoding;
309:
310: return $this;
311: }
312:
313: 314: 315: 316: 317:
318: public function getEmail()
319: {
320: return $this->email;
321: }
322:
323: 324: 325: 326: 327: 328:
329: public function setEmail(\Jyxo\Mail\Email $email)
330: {
331: $this->email = $email;
332:
333: return $this;
334: }
335:
336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346:
347: public function setSmtp($host, $port = 25, $helo = '', $user = '', $password = '', $timeout = 5)
348: {
349: $this->smtpHost = (string) $host;
350: $this->smtpPort = (int) $port;
351: $this->smtpHelo = !empty($helo) ? (string) $helo : $this->getHostname();
352: $this->smtpUser = (string) $user;
353: $this->smtpPsw = (string) $password;
354: $this->smtpTimeout = (int) $timeout;
355:
356: return $this;
357: }
358:
359: 360: 361: 362: 363: 364: 365: 366: 367:
368: public function send($mode)
369: {
370:
371: static $modes = array(
372: self::MODE_SMTP => true,
373: self::MODE_MAIL => true,
374: self::MODE_NONE => true
375: );
376: if (!isset($modes[$mode])) {
377: throw new \InvalidArgumentException(sprintf('Unknown sending mode %s.', $mode));
378: }
379: $this->mode = (string) $mode;
380:
381:
382: if (null === $this->email->from) {
383: throw new Sender\CreateException('No sender was set.');
384: }
385: if ((count($this->email->to) + count($this->email->cc) + count($this->email->bcc)) < 1) {
386: throw new Sender\CreateException('No recipient was set.');
387: }
388:
389:
390: $this->result = new Sender\Result();
391:
392:
393: $this->create();
394: $body = trim($this->createdBody);
395: if (empty($body)) {
396: throw new Sender\CreateException('No body was created.');
397: }
398:
399:
400: switch ($this->mode) {
401: case self::MODE_SMTP:
402: $this->sendBySmtp();
403: break;
404: case self::MODE_MAIL:
405: $this->sendByMail();
406: break;
407: case self::MODE_NONE:
408:
409: default:
410:
411: break;
412: }
413:
414:
415: $this->result->source = $this->getHeader() . $this->createdBody;
416:
417:
418: $this->createdHeader = array();
419: $this->createdBody = '';
420:
421: return $this->result;
422: }
423:
424: 425: 426: 427: 428:
429: private function sendByMail()
430: {
431: $recipients = '';
432: $iterator = new \ArrayIterator($this->email->to);
433: while ($iterator->valid()) {
434: $recipients .= $this->formatAddress($iterator->current());
435:
436: $iterator->next();
437: if ($iterator->valid()) {
438: $recipients .= ', ';
439: }
440: }
441:
442: $subject = $this->changeCharset($this->clearHeaderValue($this->email->subject));
443:
444: if (!mail($recipients, $this->encodeHeader($subject), $this->createdBody, $this->getHeader(array('To', 'Subject')))) {
445: throw new Sender\Exception('Could not send the message.');
446: }
447: }
448:
449: 450: 451: 452: 453:
454: private function sendBySmtp()
455: {
456: if (!class_exists('\Jyxo\Mail\Sender\Smtp')) {
457: throw new Sender\Exception('Could not sent the message. Required class \Jyxo\Mail\Sender\Smtp is missing.');
458: }
459:
460: try {
461: $smtp = new Sender\Smtp($this->smtpHost, $this->smtpPort, $this->smtpHelo, $this->smtpTimeout);
462: $smtp->connect();
463: if (!empty($this->smtpUser)) {
464: $smtp->auth($this->smtpUser, $this->smtpPsw);
465: }
466:
467:
468: $smtp->from($this->email->from->email);
469:
470:
471: $unknownRecipients = array();
472: foreach (array_merge($this->email->to, $this->email->cc, $this->email->bcc) as $recipient) {
473: try {
474: $smtp->recipient($recipient->email);
475: } catch (\Jyxo\Mail\Sender\SmtpException $e) {
476: $unknownRecipients[] = $recipient->email;
477: }
478: }
479: if (!empty($unknownRecipients)) {
480: throw new Sender\RecipientUnknownException('Unknown email recipients.', 0, $unknownRecipients);
481: }
482:
483:
484: $smtp->data($this->getHeader(), $this->createdBody);
485: $smtp->disconnect();
486: } catch (\Jyxo\Mail\Sender\RecipientUnknownException $e) {
487: $smtp->disconnect();
488: throw $e;
489: } catch (\Jyxo\Mail\Sender\SmtpException $e) {
490: $smtp->disconnect();
491: throw new Sender\Exception('Cannot send email: ' . $e->getMessage());
492: }
493: }
494:
495: 496: 497:
498: private function create()
499: {
500: $uniqueId = md5(uniqid(time()));
501: $hostname = $this->clearHeaderValue($this->getHostname());
502:
503:
504: $this->result->messageId = $uniqueId . '@' . $hostname;
505:
506:
507: $this->result->datetime = \Jyxo\Time\Time::now();
508:
509:
510: $this->boundary = array(
511: 1 => '====b1' . $uniqueId . '====' . $hostname . '====',
512: 2 => '====b2' . $uniqueId . '====' . $hostname . '===='
513: );
514:
515:
516: if (!empty($this->email->attachments)) {
517:
518: if (!empty($this->email->body->alternative)) {
519:
520: $this->type = self::TYPE_ALTERNATIVE_ATTACHMENTS;
521: } else {
522:
523: $this->type = self::TYPE_ATTACHMENTS;
524: }
525: } else {
526:
527: if (!empty($this->email->body->alternative)) {
528:
529: $this->type = self::TYPE_ALTERNATIVE;
530: } else {
531:
532: $this->type = self::TYPE_SIMPLE;
533: }
534: }
535:
536:
537: $this->createHeader();
538: $this->createBody();
539: }
540:
541: 542: 543:
544: private function createHeader()
545: {
546: $this->addHeaderLine('Date', $this->result->datetime->email);
547: $this->addHeaderLine('Return-Path', '<' . $this->clearHeaderValue($this->email->from->email) . '>');
548:
549: $this->addHeaderLine('From', $this->formatAddress($this->email->from));
550: $this->addHeaderLine('Subject', $this->encodeHeader($this->changeCharset($this->clearHeaderValue($this->email->subject))));
551:
552: if (!empty($this->email->to)) {
553: $this->addHeaderLine('To', $this->formatAddressList($this->email->to));
554: } elseif (empty($this->email->cc)) {
555:
556: $this->addHeaderLine('To', 'undisclosed-recipients:;');
557: }
558:
559: if (!empty($this->email->cc)) {
560: $this->addHeaderLine('Cc', $this->formatAddressList($this->email->cc));
561: }
562: if (!empty($this->email->bcc)) {
563: $this->addHeaderLine('Bcc', $this->formatAddressList($this->email->bcc));
564: }
565: if (!empty($this->email->replyTo)) {
566: $this->addHeaderLine('Reply-To', $this->formatAddressList($this->email->replyTo));
567: }
568:
569: if (!empty($this->email->confirmReadingTo)) {
570: $this->addHeaderLine('Disposition-Notification-To', $this->formatAddress($this->email->confirmReadingTo));
571: }
572:
573: if (!empty($this->email->priority)) {
574: $this->addHeaderLine('X-Priority', $this->email->priority);
575: }
576:
577: $this->addHeaderLine('Message-ID', '<' . $this->result->messageId . '>');
578:
579: if (!empty($this->email->inReplyTo)) {
580: $this->addHeaderLine('In-Reply-To', '<' . $this->clearHeaderValue($this->email->inReplyTo) . '>');
581: }
582: if (!empty($this->email->references)) {
583: $references = $this->email->references;
584: foreach ($references as $key => $reference) {
585: $references[$key] = $this->clearHeaderValue($reference);
586: }
587: $this->addHeaderLine('References', '<' . implode('> <', $references) . '>');
588: }
589:
590: if (!empty($this->xmailer)) {
591: $this->addHeaderLine('X-Mailer', $this->changeCharset($this->clearHeaderValue($this->xmailer)));
592: }
593:
594: $this->addHeaderLine('MIME-Version', '1.0');
595:
596:
597: foreach ($this->email->headers as $header) {
598: $this->addHeaderLine($this->changeCharset($this->clearHeaderValue($header->name)), $this->encodeHeader($this->clearHeaderValue($header->value)));
599: }
600:
601: switch ($this->type) {
602: case self::TYPE_ATTACHMENTS:
603:
604: case self::TYPE_ALTERNATIVE_ATTACHMENTS:
605: $subtype = $this->email->hasInlineAttachments() ? 'related' : 'mixed';
606: $this->addHeaderLine('Content-Type', 'multipart/' . $subtype . ';' . self::LINE_END . ' boundary="' . $this->boundary[1] . '"');
607: break;
608: case self::TYPE_ALTERNATIVE:
609: $this->addHeaderLine('Content-Type', 'multipart/alternative;' . self::LINE_END . ' boundary="' . $this->boundary[1] . '"');
610: break;
611: case self::TYPE_SIMPLE:
612:
613: default:
614: $contentType = $this->email->body->isHtml() ? 'text/html' : 'text/plain';
615:
616: $this->addHeaderLine('Content-Type', $contentType . '; charset="' . $this->clearHeaderValue($this->charset) . '"');
617: $this->addHeaderLine('Content-Transfer-Encoding', $this->encoding);
618: break;
619: }
620: }
621:
622: 623: 624:
625: private function createBody()
626: {
627: switch ($this->type) {
628: case self::TYPE_ATTACHMENTS:
629: $contentType = $this->email->body->isHtml() ? 'text/html' : 'text/plain';
630:
631: $this->createdBody .= $this->getBoundaryStart($this->boundary[1], $contentType, $this->charset, $this->encoding) . self::LINE_END;
632: $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
633:
634: $this->createdBody .= $this->attachAll();
635: break;
636: case self::TYPE_ALTERNATIVE_ATTACHMENTS:
637: $this->createdBody .= '--' . $this->boundary[1] . self::LINE_END;
638: $this->createdBody .= 'Content-Type: multipart/alternative;' . self::LINE_END . ' boundary="' . $this->boundary[2] . '"' . self::LINE_END . self::LINE_END;
639: $this->createdBody .= $this->getBoundaryStart($this->boundary[2], 'text/plain', $this->charset, $this->encoding) . self::LINE_END;
640: $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->alternative), $this->encoding) . self::LINE_END;
641: $this->createdBody .= $this->getBoundaryStart($this->boundary[2], 'text/html', $this->charset, $this->encoding) . self::LINE_END;
642: $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
643: $this->createdBody .= $this->getBoundaryEnd($this->boundary[2]) . self::LINE_END;
644:
645: $this->createdBody .= $this->attachAll();
646: break;
647: case self::TYPE_ALTERNATIVE:
648: $this->createdBody .= $this->getBoundaryStart($this->boundary[1], 'text/plain', $this->charset, $this->encoding) . self::LINE_END;
649: $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->alternative), $this->encoding) . self::LINE_END;
650: $this->createdBody .= $this->getBoundaryStart($this->boundary[1], 'text/html', $this->charset, $this->encoding) . self::LINE_END;
651: $this->createdBody .= $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding) . self::LINE_END;
652: $this->createdBody .= $this->getBoundaryEnd($this->boundary[1]);
653: break;
654: case self::TYPE_SIMPLE:
655:
656: default:
657: $this->createdBody = $this->encodeString($this->changeCharset($this->email->body->main), $this->encoding);
658: break;
659: }
660: }
661:
662: 663: 664: 665: 666:
667: private function attachAll()
668: {
669: $mime = array();
670:
671: foreach ($this->email->attachments as $attachment) {
672: $encoding = !empty($attachment->encoding) ? $attachment->encoding : Encoding::BASE64;
673: $name = $this->changeCharset($this->clearHeaderValue($attachment->name));
674:
675: $mime[] = '--' . $this->boundary[1] . self::LINE_END;
676: $mime[] = 'Content-Type: ' . $this->clearHeaderValue($attachment->mimeType) . ';' . self::LINE_END;
677: $mime[] = ' name="' . $this->encodeHeader($name) . '"' . self::LINE_END;
678: $mime[] = 'Content-Transfer-Encoding: ' . $encoding . self::LINE_END;
679:
680: if ($attachment->isInline()) {
681: $mime[] = 'Content-ID: <' . $this->clearHeaderValue($attachment->cid) . '>' . self::LINE_END;
682: }
683:
684: $mime[] = 'Content-Disposition: ' . $attachment->disposition . ';' . self::LINE_END;
685: $mime[] = ' filename="' . $this->encodeHeader($name) . '"' . self::LINE_END . self::LINE_END;
686:
687:
688: $mime[] = !empty($attachment->encoding)
689: ? \Jyxo\String::fixLineEnding($attachment->content, self::LINE_END)
690: : $this->encodeString($attachment->content, $encoding);
691: $mime[] = self::LINE_END . self::LINE_END;
692: }
693:
694: $mime[] = '--' . $this->boundary[1] . '--' . self::LINE_END;
695:
696: return implode('', $mime);
697: }
698:
699: 700: 701: 702: 703: 704: 705:
706: private function getHeader(array $except = array())
707: {
708: $header = '';
709: foreach ($this->createdHeader as $headerLine) {
710: if (!in_array($headerLine['name'], $except)) {
711: $header .= $headerLine['name'] . ': ' . $headerLine['value'] . self::LINE_END;
712: }
713: }
714:
715: return $header . self::LINE_END;
716: }
717:
718: 719: 720: 721: 722: 723:
724: private function formatAddress(\Jyxo\Mail\Email\Address $address)
725: {
726: $name = $this->changeCharset($this->clearHeaderValue($address->name));
727: $email = $this->clearHeaderValue($address->email);
728:
729:
730: if ((empty($name)) || ($name === $email)) {
731: return $email;
732: }
733:
734: if (preg_match('~[\200-\377]~', $name)) {
735:
736: $name = $this->encodeHeader($name);
737: } elseif (preg_match('~[^\w\s!#\$%&\'*+/=?^_`{|}\~-]~', $name)) {
738:
739: $name = '"' . $name . '"';
740: }
741:
742: return $name . ' <' . $email . '>';
743: }
744:
745: 746: 747: 748: 749: 750:
751: private function formatAddressList(array $addressList)
752: {
753: $formated = '';
754: while ($address = current($addressList)) {
755: $formated .= $this->formatAddress($address);
756:
757: if (false !== next($addressList)) {
758: $formated .= ', ';
759: }
760: }
761: return $formated;
762: }
763:
764: 765: 766: 767: 768: 769:
770: private function addHeaderLine($name, $value)
771: {
772: $this->createdHeader[] = array(
773: 'name' => $name,
774: 'value' => trim($value)
775: );
776: }
777:
778: 779: 780: 781: 782: 783:
784: private function encodeHeader($string)
785: {
786:
787: $count = preg_match_all('~[^\040-\176]~', $string, $matches);
788: if (0 === $count) {
789: return $string;
790: }
791:
792:
793: $maxlen = 75 - 7 - strlen($this->charset);
794:
795:
796: 797: 798: 799: 800: 801: 802: 803: 804: 805: 806:
807:
808:
809: $encoding = 'B';
810: $maxlen -= $maxlen % 4;
811: $encoded = $this->encodeString($string, Encoding::BASE64, $maxlen);
812:
813:
814: $encoded = trim(preg_replace('~^(.*)$~m', ' =?' . $this->clearHeaderValue($this->charset) . '?' . $encoding . '?\1?=', $encoded));
815:
816: return $encoded;
817: }
818:
819: 820: 821: 822: 823: 824: 825: 826:
827: private function encodeString($string, $encoding, $lineLength = self::LINE_LENGTH)
828: {
829: return Encoding::encode($string, $encoding, $lineLength, self::LINE_END);
830: }
831:
832: 833: 834: 835: 836: 837: 838: 839: 840:
841: private function getBoundaryStart($boundary, $contentType, $charset, $encoding)
842: {
843: $start = '--' . $boundary . self::LINE_END;
844: $start .= 'Content-Type: ' . $contentType . '; charset="' . $this->clearHeaderValue($charset) . '"' . self::LINE_END;
845: $start .= 'Content-Transfer-Encoding: ' . $encoding . self::LINE_END;
846:
847: return $start;
848: }
849:
850: 851: 852: 853: 854: 855:
856: private function getBoundaryEnd($boundary)
857: {
858: return self::LINE_END . '--' . $boundary . '--' . self::LINE_END;
859: }
860:
861: 862: 863: 864: 865: 866:
867: private function clearHeaderValue($string)
868: {
869: return strtr(trim($string), "\r\n\t", ' ');
870: }
871:
872: 873: 874: 875: 876: 877:
878: private function changeCharset($string)
879: {
880: if ('utf-8' !== $this->charset) {
881:
882: $string = @iconv('utf-8', $this->charset . '//TRANSLIT', $string);
883: }
884:
885: return $string;
886: }
887: }
888: