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