1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Mail;
9:
10: use Nette;
11: use Nette\Utils\Strings;
12:
13:
14: 15: 16: 17: 18: 19:
20: class Message extends MimePart
21: {
22:
23: const HIGH = 1,
24: NORMAL = 3,
25: LOW = 5;
26:
27:
28: public static = [
29: 'MIME-Version' => '1.0',
30: 'X-Mailer' => 'Nette Framework',
31: ];
32:
33:
34: private $attachments = [];
35:
36:
37: private $inlines = [];
38:
39:
40: private $htmlBody = '';
41:
42:
43: public function __construct()
44: {
45: foreach (static::$defaultHeaders as $name => $value) {
46: $this->setHeader($name, $value);
47: }
48: $this->setHeader('Date', date('r'));
49: }
50:
51:
52: 53: 54: 55: 56: 57:
58: public function setFrom($email, $name = null)
59: {
60: $this->setHeader('From', $this->formatEmail($email, $name));
61: return $this;
62: }
63:
64:
65: 66: 67: 68:
69: public function getFrom()
70: {
71: return $this->getHeader('From');
72: }
73:
74:
75: 76: 77: 78: 79: 80:
81: public function addReplyTo($email, $name = null)
82: {
83: $this->setHeader('Reply-To', $this->formatEmail($email, $name), true);
84: return $this;
85: }
86:
87:
88: 89: 90: 91: 92:
93: public function setSubject($subject)
94: {
95: $this->setHeader('Subject', $subject);
96: return $this;
97: }
98:
99:
100: 101: 102: 103:
104: public function getSubject()
105: {
106: return $this->getHeader('Subject');
107: }
108:
109:
110: 111: 112: 113: 114: 115:
116: public function addTo($email, $name = null)
117: {
118: $this->setHeader('To', $this->formatEmail($email, $name), true);
119: return $this;
120: }
121:
122:
123: 124: 125: 126: 127: 128:
129: public function addCc($email, $name = null)
130: {
131: $this->setHeader('Cc', $this->formatEmail($email, $name), true);
132: return $this;
133: }
134:
135:
136: 137: 138: 139: 140: 141:
142: public function addBcc($email, $name = null)
143: {
144: $this->setHeader('Bcc', $this->formatEmail($email, $name), true);
145: return $this;
146: }
147:
148:
149: 150: 151: 152: 153: 154:
155: private function formatEmail($email, $name)
156: {
157: if (!$name && preg_match('#^(.+) +<(.*)>\z#', $email, $matches)) {
158: return [$matches[2] => $matches[1]];
159: } else {
160: return [$email => $name];
161: }
162: }
163:
164:
165: 166: 167: 168: 169:
170: public function setReturnPath($email)
171: {
172: $this->setHeader('Return-Path', $email);
173: return $this;
174: }
175:
176:
177: 178: 179: 180:
181: public function getReturnPath()
182: {
183: return $this->getHeader('Return-Path');
184: }
185:
186:
187: 188: 189: 190: 191:
192: public function setPriority($priority)
193: {
194: $this->setHeader('X-Priority', (int) $priority);
195: return $this;
196: }
197:
198:
199: 200: 201: 202:
203: public function getPriority()
204: {
205: return $this->getHeader('X-Priority');
206: }
207:
208:
209: 210: 211: 212: 213: 214:
215: public function setHtmlBody($html, $basePath = null)
216: {
217: $html = (string) $html;
218:
219: if ($basePath) {
220: $cids = [];
221: $matches = Strings::matchAll(
222: $html,
223: '#
224: (<img[^<>]*\s src\s*=\s*
225: |<body[^<>]*\s background\s*=\s*
226: |<[^<>]+\s style\s*=\s* ["\'][^"\'>]+[:\s] url\(
227: |<style[^>]*>[^<]+ [:\s] url\()
228: (["\']?)(?![a-z]+:|[/\\#])([^"\'>)\s]+)
229: |\[\[ ([\w()+./@~-]+) \]\]
230: #ix',
231: PREG_OFFSET_CAPTURE
232: );
233: foreach (array_reverse($matches) as $m) {
234: $file = rtrim($basePath, '/\\') . '/' . (isset($m[4]) ? $m[4][0] : urldecode($m[3][0]));
235: if (!isset($cids[$file])) {
236: $cids[$file] = substr($this->addEmbeddedFile($file)->getHeader('Content-ID'), 1, -1);
237: }
238: $html = substr_replace($html,
239: "{$m[1][0]}{$m[2][0]}cid:{$cids[$file]}",
240: $m[0][1], strlen($m[0][0])
241: );
242: }
243: }
244:
245: if ($this->getSubject() == null) {
246: $html = Strings::replace($html, '#<title>(.+?)</title>#is', function ($m) {
247: $this->setSubject(html_entity_decode($m[1], ENT_QUOTES, 'UTF-8'));
248: });
249: }
250:
251: $this->htmlBody = ltrim(str_replace("\r", '', $html), "\n");
252:
253: if ($this->getBody() === '' && $html !== '') {
254: $this->setBody($this->buildText($html));
255: }
256:
257: return $this;
258: }
259:
260:
261: 262: 263: 264:
265: public function getHtmlBody()
266: {
267: return $this->htmlBody;
268: }
269:
270:
271: 272: 273: 274: 275: 276: 277:
278: public function addEmbeddedFile($file, $content = null, $contentType = null)
279: {
280: return $this->inlines[$file] = $this->createAttachment($file, $content, $contentType, 'inline')
281: ->setHeader('Content-ID', $this->getRandomId());
282: }
283:
284:
285: 286: 287: 288: 289:
290: public function addInlinePart(MimePart $part)
291: {
292: $this->inlines[] = $part;
293: return $this;
294: }
295:
296:
297: 298: 299: 300: 301: 302: 303:
304: public function addAttachment($file, $content = null, $contentType = null)
305: {
306: return $this->attachments[] = $this->createAttachment($file, $content, $contentType, 'attachment');
307: }
308:
309:
310: 311: 312: 313:
314: public function getAttachments()
315: {
316: return $this->attachments;
317: }
318:
319:
320: 321: 322: 323: 324: 325: 326: 327:
328: private function createAttachment($file, $content, $contentType, $disposition)
329: {
330: $part = new MimePart;
331: if ($content === null) {
332: $content = @file_get_contents($file);
333: if ($content === false) {
334: throw new Nette\FileNotFoundException("Unable to read file '$file'.");
335: }
336: } else {
337: $content = (string) $content;
338: }
339: if (!$contentType) {
340: $contentType = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $content);
341: }
342: if (!strcasecmp($contentType, 'message/rfc822')) {
343: $contentType = 'application/octet-stream';
344: }
345:
346: $part->setBody($content);
347: $part->setContentType($contentType);
348: $part->setEncoding(preg_match('#(multipart|message)/#A', $contentType) ? self::ENCODING_8BIT : self::ENCODING_BASE64);
349: $part->setHeader('Content-Disposition', $disposition . '; filename="' . Strings::fixEncoding(basename($file)) . '"');
350: return $part;
351: }
352:
353:
354:
355:
356:
357: 358: 359: 360:
361: public function generateMessage()
362: {
363: return $this->build()->getEncodedMessage();
364: }
365:
366:
367: 368: 369: 370:
371: protected function build()
372: {
373: $mail = clone $this;
374: $mail->setHeader('Message-ID', $this->getRandomId());
375:
376: $cursor = $mail;
377: if ($mail->attachments) {
378: $tmp = $cursor->setContentType('multipart/mixed');
379: $cursor = $cursor->addPart();
380: foreach ($mail->attachments as $value) {
381: $tmp->addPart($value);
382: }
383: }
384:
385: if ($mail->htmlBody !== '') {
386: $tmp = $cursor->setContentType('multipart/alternative');
387: $cursor = $cursor->addPart();
388: $alt = $tmp->addPart();
389: if ($mail->inlines) {
390: $tmp = $alt->setContentType('multipart/related');
391: $alt = $alt->addPart();
392: foreach ($mail->inlines as $value) {
393: $tmp->addPart($value);
394: }
395: }
396: $alt->setContentType('text/html', 'UTF-8')
397: ->setEncoding(preg_match('#[^\n]{990}#', $mail->htmlBody)
398: ? self::ENCODING_QUOTED_PRINTABLE
399: : (preg_match('#[\x80-\xFF]#', $mail->htmlBody) ? self::ENCODING_8BIT : self::ENCODING_7BIT))
400: ->setBody($mail->htmlBody);
401: }
402:
403: $text = $mail->getBody();
404: $mail->setBody('');
405: $cursor->setContentType('text/plain', 'UTF-8')
406: ->setEncoding(preg_match('#[^\n]{990}#', $text)
407: ? self::ENCODING_QUOTED_PRINTABLE
408: : (preg_match('#[\x80-\xFF]#', $text) ? self::ENCODING_8BIT : self::ENCODING_7BIT))
409: ->setBody($text);
410:
411: return $mail;
412: }
413:
414:
415: 416: 417: 418: 419:
420: protected function buildText($html)
421: {
422: $text = Strings::replace($html, [
423: '#<(style|script|head).*</\\1>#Uis' => '',
424: '#<t[dh][ >]#i' => ' $0',
425: '#<a\s[^>]*href=(?|"([^"]+)"|\'([^\']+)\')[^>]*>(.*?)</a>#is' => '$2 <$1>',
426: '#[\r\n]+#' => ' ',
427: '#<(/?p|/?h\d|li|br|/tr)[ >/]#i' => "\n$0",
428: ]);
429: $text = html_entity_decode(strip_tags($text), ENT_QUOTES, 'UTF-8');
430: $text = Strings::replace($text, '#[ \t]+#', ' ');
431: return trim($text);
432: }
433:
434:
435:
436: private function getRandomId()
437: {
438: return '<' . Nette\Utils\Random::generate() . '@'
439: . preg_replace('#[^\w.-]+#', '', isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : php_uname('n'))
440: . '>';
441: }
442: }
443: