Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
139 / 139 |
|
100.00% |
48 / 48 |
CRAP | |
100.00% |
1 / 1 |
Message | |
100.00% |
139 / 139 |
|
100.00% |
48 / 48 |
64 | |
100.00% |
1 / 1 |
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setMailer | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCrlf | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getCharset | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setBoundary | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getBoundary | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
setHeader | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getHeaderLines | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
renderHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
prepareHeaders | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
renderData | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
1 | |||
setPlainMessage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getPlainMessage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderPlainMessage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
setHtmlMessage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getHtmlMessage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderHtmlMessage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
renderMessage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
1 | |||
getAttachments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addAttachment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setInlineAttachment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getInlineAttachments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderAttachments | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 | |||
getContentType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
renderInlineAttachments | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
setSubject | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getSubject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addTo | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addCc | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getCc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRecipients | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addBcc | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getBcc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addReplyTo | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getReplyTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFrom | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFromAddress | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFromName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setDate | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getDate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setXPriority | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getXPriority | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
formatAddress | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
formatAddressList | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework Email Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
6 | * |
7 | * For the full copyright and license information, please view the LICENSE |
8 | * file that was distributed with this source code. |
9 | */ |
10 | namespace Framework\Email; |
11 | |
12 | use DateTime; |
13 | use JetBrains\PhpStorm\Language; |
14 | use LogicException; |
15 | |
16 | /** |
17 | * Class Message. |
18 | * |
19 | * @package email |
20 | */ |
21 | class Message implements \Stringable |
22 | { |
23 | /** |
24 | * The Mailer instance. |
25 | * |
26 | * @var Mailer |
27 | */ |
28 | protected Mailer $mailer; |
29 | /** |
30 | * The message boundary. |
31 | * |
32 | * @var string |
33 | */ |
34 | protected string $boundary; |
35 | /** |
36 | * @var array<string,string> |
37 | */ |
38 | protected array $headers = [ |
39 | 'mime-version' => '1.0', |
40 | ]; |
41 | /** |
42 | * A list of attachments with Content-Disposition equals `attachment`. |
43 | * |
44 | * @var array<int,string> The filenames |
45 | */ |
46 | protected array $attachments = []; |
47 | /** |
48 | * An associative array of attachments with Content-Disposition equals `inline`. |
49 | * |
50 | * @var array<string,string> The Content-ID's as keys and the filenames as values |
51 | */ |
52 | protected array $inlineAttachments = []; |
53 | /** |
54 | * The plain text message. |
55 | * |
56 | * @var string |
57 | */ |
58 | protected string $plainMessage; |
59 | /** |
60 | * The HTML message. |
61 | * |
62 | * @var string |
63 | */ |
64 | protected string $htmlMessage; |
65 | /** |
66 | * An associative array used in the `To` header. |
67 | * |
68 | * @var array<string,string|null> The email addresses as keys and the optional |
69 | * name as values |
70 | */ |
71 | protected array $to = []; |
72 | /** |
73 | * An associative array used in the `Cc` header. |
74 | * |
75 | * @var array<string,string|null> The email addresses as keys and the optional |
76 | * name as values |
77 | */ |
78 | protected array $cc = []; |
79 | /** |
80 | * An associative array used in the `Bcc` header. |
81 | * |
82 | * @var array<string,string|null> The email addresses as keys and the optional |
83 | * name as values |
84 | */ |
85 | protected array $bcc = []; |
86 | /** |
87 | * An associative array used in the `Reply-To` header. |
88 | * |
89 | * @var array<string,string|null> The email addresses as keys and the optional |
90 | * name as values |
91 | */ |
92 | protected array $replyTo = []; |
93 | /** |
94 | * The values used in the `From` header. |
95 | * |
96 | * @var array<int,string|null> The email address as in the index 0 and the |
97 | * optional name in the index 1 |
98 | */ |
99 | protected array $from = []; |
100 | /** |
101 | * The message Date. |
102 | * |
103 | * @var string|null |
104 | */ |
105 | protected ?string $date = null; |
106 | /** |
107 | * The message X-Priority. |
108 | * |
109 | * @var XPriority |
110 | */ |
111 | protected XPriority $xPriority; |
112 | |
113 | public function __toString() : string |
114 | { |
115 | return $this->renderData(); |
116 | } |
117 | |
118 | /** |
119 | * @param Mailer $mailer The Mailer instance |
120 | * |
121 | * @return static |
122 | */ |
123 | public function setMailer(Mailer $mailer) : static |
124 | { |
125 | $this->mailer = $mailer; |
126 | return $this; |
127 | } |
128 | |
129 | protected function getCrlf() : string |
130 | { |
131 | if (isset($this->mailer)) { |
132 | return $this->mailer->getCrlf(); |
133 | } |
134 | return "\r\n"; |
135 | } |
136 | |
137 | protected function getCharset() : string |
138 | { |
139 | if (isset($this->mailer)) { |
140 | return $this->mailer->getCharset(); |
141 | } |
142 | return 'utf-8'; |
143 | } |
144 | |
145 | public function setBoundary(string $boundary = null) : static |
146 | { |
147 | $this->boundary = $boundary ?? \bin2hex(\random_bytes(16)); |
148 | return $this; |
149 | } |
150 | |
151 | public function getBoundary() : string |
152 | { |
153 | if ( ! isset($this->boundary)) { |
154 | $this->setBoundary(); |
155 | } |
156 | return $this->boundary; |
157 | } |
158 | |
159 | /** |
160 | * @param string $name |
161 | * @param string $value |
162 | * |
163 | * @return static |
164 | */ |
165 | public function setHeader(string $name, string $value) : static |
166 | { |
167 | $this->headers[\strtolower($name)] = $value; |
168 | return $this; |
169 | } |
170 | |
171 | public function getHeader(string $name) : ?string |
172 | { |
173 | return $this->headers[\strtolower($name)] ?? null; |
174 | } |
175 | |
176 | /** |
177 | * @return array<string,string> |
178 | */ |
179 | public function getHeaders() : array |
180 | { |
181 | return $this->headers; |
182 | } |
183 | |
184 | /** |
185 | * @return array<int,string> |
186 | */ |
187 | public function getHeaderLines() : array |
188 | { |
189 | $lines = []; |
190 | foreach ($this->getHeaders() as $name => $value) { |
191 | $lines[] = Header::getName($name) . ': ' . $value; |
192 | } |
193 | return $lines; |
194 | } |
195 | |
196 | protected function renderHeaders() : string |
197 | { |
198 | return \implode($this->getCrlf(), $this->getHeaderLines()); |
199 | } |
200 | |
201 | protected function prepareHeaders() : void |
202 | { |
203 | if ( ! $this->getDate()) { |
204 | $this->setDate(); |
205 | } |
206 | $multipart = $this->getInlineAttachments() ? 'related' : 'mixed'; |
207 | $this->setHeader( |
208 | Header::CONTENT_TYPE, |
209 | 'multipart/' . $multipart . '; boundary="mixed-' . $this->getBoundary() . '"' |
210 | ); |
211 | } |
212 | |
213 | protected function renderData() : string |
214 | { |
215 | $boundary = $this->getBoundary(); |
216 | $crlf = $this->getCrlf(); |
217 | $this->prepareHeaders(); |
218 | $data = $this->renderHeaders() . $crlf . $crlf; |
219 | $data .= '--mixed-' . $boundary . $crlf; |
220 | $data .= 'Content-Type: multipart/alternative; boundary="alt-' . $boundary . '"' |
221 | . $crlf . $crlf; |
222 | $data .= $this->renderPlainMessage(); |
223 | $data .= $this->renderHtmlMessage(); |
224 | $data .= '--alt-' . $boundary . '--' . $crlf . $crlf; |
225 | $data .= $this->renderAttachments(); |
226 | $data .= $this->renderInlineAttachments(); |
227 | $data .= '--mixed-' . $boundary . '--'; |
228 | return $data; |
229 | } |
230 | |
231 | /** |
232 | * @param string $message |
233 | * |
234 | * @return static |
235 | */ |
236 | public function setPlainMessage(string $message) : static |
237 | { |
238 | $this->plainMessage = $message; |
239 | return $this; |
240 | } |
241 | |
242 | public function getPlainMessage() : ?string |
243 | { |
244 | return $this->plainMessage ?? null; |
245 | } |
246 | |
247 | protected function renderPlainMessage() : ?string |
248 | { |
249 | $message = $this->getPlainMessage(); |
250 | return $message !== null ? $this->renderMessage($message, 'text/plain') : null; |
251 | } |
252 | |
253 | /** |
254 | * @param string $message |
255 | * |
256 | * @return static |
257 | */ |
258 | public function setHtmlMessage(#[Language('HTML')] string $message) : static |
259 | { |
260 | $this->htmlMessage = $message; |
261 | return $this; |
262 | } |
263 | |
264 | public function getHtmlMessage() : ?string |
265 | { |
266 | return $this->htmlMessage ?? null; |
267 | } |
268 | |
269 | protected function renderHtmlMessage() : ?string |
270 | { |
271 | $message = $this->getHtmlMessage(); |
272 | return $message !== null ? $this->renderMessage($message) : null; |
273 | } |
274 | |
275 | protected function renderMessage( |
276 | string $message, |
277 | string $contentType = 'text/html' |
278 | ) : string { |
279 | $message = \base64_encode($message); |
280 | $crlf = $this->getCrlf(); |
281 | $part = '--alt-' . $this->getBoundary() . $crlf; |
282 | $part .= 'Content-Type: ' . $contentType . '; charset=' |
283 | . $this->getCharset() . $crlf; |
284 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; |
285 | $part .= \chunk_split($message) . $crlf; |
286 | return $part; |
287 | } |
288 | |
289 | /** |
290 | * @return array<int,string> |
291 | */ |
292 | public function getAttachments() : array |
293 | { |
294 | return $this->attachments; |
295 | } |
296 | |
297 | /** |
298 | * @param string $filename The filename |
299 | * |
300 | * @return static |
301 | */ |
302 | public function addAttachment(string $filename) : static |
303 | { |
304 | $this->attachments[] = $filename; |
305 | return $this; |
306 | } |
307 | |
308 | /** |
309 | * @param string $filename The filename |
310 | * @param string $cid The Content-ID |
311 | * |
312 | * @return static |
313 | */ |
314 | public function setInlineAttachment(string $filename, string $cid) : static |
315 | { |
316 | $this->inlineAttachments[$cid] = $filename; |
317 | return $this; |
318 | } |
319 | |
320 | /** |
321 | * @return array<string,string> |
322 | */ |
323 | public function getInlineAttachments() : array |
324 | { |
325 | return $this->inlineAttachments; |
326 | } |
327 | |
328 | protected function renderAttachments() : string |
329 | { |
330 | $part = ''; |
331 | $crlf = $this->getCrlf(); |
332 | foreach ($this->getAttachments() as $attachment) { |
333 | if ( ! \is_file($attachment)) { |
334 | throw new LogicException('Attachment file not found: ' . $attachment); |
335 | } |
336 | $filename = \pathinfo($attachment, \PATHINFO_BASENAME); |
337 | $filename = \htmlspecialchars($filename, \ENT_QUOTES | \ENT_HTML5); |
338 | $contents = \file_get_contents($attachment); |
339 | $contents = \base64_encode($contents); // @phpstan-ignore-line |
340 | $part .= '--mixed-' . $this->getBoundary() . $crlf; |
341 | $part .= 'Content-Type: ' . $this->getContentType($attachment) |
342 | . '; name="' . $filename . '"' . $crlf; |
343 | $part .= 'Content-Disposition: attachment; filename="' . $filename . '"' . $crlf; |
344 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; |
345 | $part .= \chunk_split($contents) . $crlf; |
346 | } |
347 | return $part; |
348 | } |
349 | |
350 | protected function getContentType(string $filename) : string |
351 | { |
352 | return \mime_content_type($filename) ?: 'application/octet-stream'; |
353 | } |
354 | |
355 | protected function renderInlineAttachments() : string |
356 | { |
357 | $part = ''; |
358 | $crlf = $this->getCrlf(); |
359 | foreach ($this->getInlineAttachments() as $cid => $filename) { |
360 | if ( ! \is_file($filename)) { |
361 | throw new LogicException('Inline attachment file not found: ' . $filename); |
362 | } |
363 | $contents = \file_get_contents($filename); |
364 | $contents = \base64_encode($contents); // @phpstan-ignore-line |
365 | $part .= '--mixed-' . $this->getBoundary() . $crlf; |
366 | $part .= 'Content-ID: ' . $cid . $crlf; |
367 | $part .= 'Content-Type: ' . $this->getContentType($filename) . $crlf; |
368 | $part .= 'Content-Disposition: inline' . $crlf; |
369 | $part .= 'Content-Transfer-Encoding: base64' . $crlf . $crlf; |
370 | $part .= \chunk_split($contents) . $crlf; |
371 | } |
372 | return $part; |
373 | } |
374 | |
375 | /** |
376 | * @param string $subject |
377 | * |
378 | * @return static |
379 | */ |
380 | public function setSubject(string $subject) : static |
381 | { |
382 | $this->setHeader(Header::SUBJECT, $subject); |
383 | return $this; |
384 | } |
385 | |
386 | public function getSubject() : ?string |
387 | { |
388 | return $this->getHeader(Header::SUBJECT); |
389 | } |
390 | |
391 | /** |
392 | * @param string $address |
393 | * @param string|null $name |
394 | * |
395 | * @return static |
396 | */ |
397 | public function addTo(string $address, string $name = null) : static |
398 | { |
399 | $this->to[$address] = $name; |
400 | $this->setHeader(Header::TO, static::formatAddressList($this->to)); |
401 | return $this; |
402 | } |
403 | |
404 | /** |
405 | * @return array<string,string|null> |
406 | */ |
407 | public function getTo() : array |
408 | { |
409 | return $this->to; |
410 | } |
411 | |
412 | /** |
413 | * Add Carbon Copy email address. |
414 | * |
415 | * @param string $address |
416 | * @param string|null $name |
417 | * |
418 | * @return static |
419 | */ |
420 | public function addCc(string $address, string $name = null) : static |
421 | { |
422 | $this->cc[$address] = $name; |
423 | $this->setHeader(Header::CC, static::formatAddressList($this->cc)); |
424 | return $this; |
425 | } |
426 | |
427 | /** |
428 | * @return array<string,string|null> |
429 | */ |
430 | public function getCc() : array |
431 | { |
432 | return $this->cc; |
433 | } |
434 | |
435 | /** |
436 | * @return array<int,string> |
437 | */ |
438 | public function getRecipients() : array |
439 | { |
440 | $recipients = \array_replace($this->getTo(), $this->getCc()); |
441 | return \array_keys($recipients); |
442 | } |
443 | |
444 | /** |
445 | * Add Blind Carbon Copy email address. |
446 | * |
447 | * @param string $address |
448 | * @param string|null $name |
449 | * |
450 | * @return static |
451 | */ |
452 | public function addBcc(string $address, string $name = null) : static |
453 | { |
454 | $this->bcc[$address] = $name; |
455 | $this->setHeader(Header::BCC, static::formatAddressList($this->bcc)); |
456 | return $this; |
457 | } |
458 | |
459 | /** |
460 | * @return array<string,string|null> |
461 | */ |
462 | public function getBcc() : array |
463 | { |
464 | return $this->bcc; |
465 | } |
466 | |
467 | /** |
468 | * @param string $address |
469 | * @param string|null $name |
470 | * |
471 | * @return static |
472 | */ |
473 | public function addReplyTo(string $address, string $name = null) : static |
474 | { |
475 | $this->replyTo[$address] = $name; |
476 | $this->setHeader(Header::REPLY_TO, static::formatAddressList($this->replyTo)); |
477 | return $this; |
478 | } |
479 | |
480 | /** |
481 | * @return array<string,string|null> |
482 | */ |
483 | public function getReplyTo() : array |
484 | { |
485 | return $this->replyTo; |
486 | } |
487 | |
488 | /** |
489 | * @param string $address |
490 | * @param string|null $name |
491 | * |
492 | * @return static |
493 | */ |
494 | public function setFrom(string $address, string $name = null) : static |
495 | { |
496 | $this->from = [$address, $name]; |
497 | $this->setHeader(Header::FROM, static::formatAddress($address, $name)); |
498 | return $this; |
499 | } |
500 | |
501 | /** |
502 | * @return array<int,string|null> |
503 | */ |
504 | public function getFrom() : array |
505 | { |
506 | return $this->from; |
507 | } |
508 | |
509 | public function getFromAddress() : ?string |
510 | { |
511 | return $this->from[0] ?? null; |
512 | } |
513 | |
514 | public function getFromName() : ?string |
515 | { |
516 | return $this->from[1] ?? null; |
517 | } |
518 | |
519 | /** |
520 | * @param DateTime|null $datetime |
521 | * |
522 | * @return static |
523 | */ |
524 | public function setDate(DateTime $datetime = null) : static |
525 | { |
526 | $date = $datetime ? $datetime->format('r') : \date('r'); |
527 | $this->setHeader(Header::DATE, $date); |
528 | return $this; |
529 | } |
530 | |
531 | public function getDate() : ?string |
532 | { |
533 | return $this->getHeader(Header::DATE); |
534 | } |
535 | |
536 | /** |
537 | * @param XPriority $priority |
538 | * |
539 | * @return static |
540 | */ |
541 | public function setXPriority(XPriority $priority) : static |
542 | { |
543 | $this->xPriority = $priority; |
544 | $this->setHeader(Header::X_PRIORITY, (string) $priority->value); |
545 | return $this; |
546 | } |
547 | |
548 | public function getXPriority() : ?XPriority |
549 | { |
550 | return $this->xPriority ?? null; |
551 | } |
552 | |
553 | protected static function formatAddress(string $address, string $name = null) : string |
554 | { |
555 | return $name !== null ? '"' . $name . '" <' . $address . '>' : $address; |
556 | } |
557 | |
558 | /** |
559 | * @param array<string,string|null> $addresses |
560 | * |
561 | * @return string |
562 | */ |
563 | protected static function formatAddressList(array $addresses) : string |
564 | { |
565 | $data = []; |
566 | foreach ($addresses as $address => $name) { |
567 | $data[] = static::formatAddress($address, $name); |
568 | } |
569 | return \implode(', ', $data); |
570 | } |
571 | } |