Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.54% |
238 / 244 |
|
96.00% |
48 / 50 |
CRAP | |
0.00% |
0 / 1 |
Message | |
97.54% |
238 / 244 |
|
96.00% |
48 / 50 |
131 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
load | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
setSubject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setTo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setCc | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setBcc | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setFrom | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setReplyTo | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setSender | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setReturnPath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setBody | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addText | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
addHtml | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
attachFile | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
attachFileFromStream | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
addPart | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
removeHeader | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSubject | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBcc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReplyTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSender | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReturnPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasCc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasBcc | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasReplyTo | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasSender | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasReturnPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasAttachments | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
getBody | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
6.17 | |||
getBoundary | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getPart | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParts | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setBoundary | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
generateBoundary | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
render | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
save | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renderAsLines | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
toByteStream | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
parseFromFile | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
parse | |
100.00% |
53 / 53 |
|
100.00% |
1 / 1 |
22 | |||
decodeText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
parseAddresses | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
17 | |||
parseNameAndEmail | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
validateContentType | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
8 | |||
__clone | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (http://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@nolainteractive.com> |
7 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
8 | * @license http://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Mail; |
15 | |
16 | use Pop\Mail\Message\Part; |
17 | use Pop\Mail\Message\Simple; |
18 | use Pop\Mail\Message\Text; |
19 | use Pop\Mail\Message\Html; |
20 | use Pop\Mail\Message\Attachment; |
21 | use Pop\Mail\Message\AbstractMessage; |
22 | use Pop\Mail\Message\AbstractPart; |
23 | use Pop\Mail\Message\PartInterface; |
24 | use Pop\Mime\Part\Header\Value; |
25 | |
26 | /** |
27 | * Message class |
28 | * |
29 | * @category Pop |
30 | * @package Pop\Mail |
31 | * @author Nick Sagona, III <dev@nolainteractive.com> |
32 | * @copyright Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com) |
33 | * @license http://www.popphp.org/license New BSD License |
34 | * @version 4.0.0 |
35 | */ |
36 | class Message extends AbstractMessage |
37 | { |
38 | |
39 | /** |
40 | * Message newline constant |
41 | * @var string |
42 | */ |
43 | const CRLF = "\r\n"; |
44 | |
45 | /** |
46 | * Message parts |
47 | * @var array |
48 | */ |
49 | protected array $parts = []; |
50 | |
51 | /** |
52 | * Message addresses |
53 | * @var array |
54 | */ |
55 | protected array $addresses = [ |
56 | 'To' => [], |
57 | 'CC' => [], |
58 | 'BCC' => [], |
59 | 'From' => [], |
60 | 'Reply-To' => [], |
61 | 'Sender' => [], |
62 | 'Return-Path' => [] |
63 | ]; |
64 | |
65 | /** |
66 | * Message boundary |
67 | * @var ?string |
68 | */ |
69 | protected ?string $boundary = null; |
70 | |
71 | /** |
72 | * Constructor |
73 | * |
74 | * Instantiate the message object |
75 | * |
76 | * @param ?string $subject |
77 | */ |
78 | public function __construct(?string $subject = null) |
79 | { |
80 | parent::__construct(); |
81 | |
82 | if ($subject !== null) { |
83 | $this->setSubject($subject); |
84 | } |
85 | } |
86 | |
87 | /** |
88 | * Load a message from a string source or file on disk |
89 | * |
90 | * @param string $message |
91 | * @throws Exception |
92 | * @return Message |
93 | */ |
94 | public static function load(string $message): Message |
95 | { |
96 | if (is_string($message) && (str_contains($message, 'Subject:'))) { |
97 | return self::parse($message); |
98 | } else if (file_exists($message)) { |
99 | return self::parseFromFile($message); |
100 | } else { |
101 | throw new Exception('Error: Unable to parse message content'); |
102 | } |
103 | } |
104 | |
105 | /** |
106 | * Set Subject |
107 | * |
108 | * @param string $subject |
109 | * @return AbstractMessage |
110 | */ |
111 | public function setSubject(string $subject): AbstractMessage |
112 | { |
113 | return $this->addHeader('Subject', $subject); |
114 | } |
115 | |
116 | /** |
117 | * Set To |
118 | * |
119 | * @param mixed $to |
120 | * @return AbstractMessage |
121 | */ |
122 | public function setTo(mixed $to): AbstractMessage |
123 | { |
124 | if ($to instanceof Value) { |
125 | $to = (string)$to; |
126 | } |
127 | $this->addresses['To'] = $this->parseAddresses($to, true); |
128 | return $this->addHeader('To', $this->parseAddresses($to)); |
129 | } |
130 | |
131 | /** |
132 | * Set CC |
133 | * |
134 | * @param mixed $cc |
135 | * @return AbstractMessage |
136 | */ |
137 | public function setCc(mixed $cc): AbstractMessage |
138 | { |
139 | if ($cc instanceof Value) { |
140 | $cc = (string)$cc; |
141 | } |
142 | $this->addresses['CC'] = $this->parseAddresses($cc, true); |
143 | return $this->addHeader('CC', $this->parseAddresses($cc)); |
144 | } |
145 | |
146 | /** |
147 | * Set BCC |
148 | * |
149 | * @param mixed $bcc |
150 | * @return AbstractMessage |
151 | */ |
152 | public function setBcc(mixed $bcc): AbstractMessage |
153 | { |
154 | if ($bcc instanceof Value) { |
155 | $bcc = (string)$bcc; |
156 | } |
157 | $this->addresses['BCC'] = $this->parseAddresses($bcc, true); |
158 | return $this->addHeader('BCC', $this->parseAddresses($bcc)); |
159 | } |
160 | |
161 | /** |
162 | * Set From |
163 | * |
164 | * @param mixed $from |
165 | * @return AbstractMessage |
166 | */ |
167 | public function setFrom(mixed $from): AbstractMessage |
168 | { |
169 | if ($from instanceof Value) { |
170 | $from = (string)$from; |
171 | } |
172 | $this->addresses['From'] = $this->parseAddresses($from, true); |
173 | return $this->addHeader('From', $this->parseAddresses($from)); |
174 | } |
175 | |
176 | /** |
177 | * Set Reply-To |
178 | * |
179 | * @param mixed $replyTo |
180 | * @return AbstractMessage |
181 | */ |
182 | public function setReplyTo(mixed $replyTo): AbstractMessage |
183 | { |
184 | if ($replyTo instanceof Value) { |
185 | $replyTo = (string)$replyTo; |
186 | } |
187 | $this->addresses['Reply-To'] = $this->parseAddresses($replyTo, true); |
188 | return $this->addHeader('Reply-To', $this->parseAddresses($replyTo)); |
189 | } |
190 | |
191 | /** |
192 | * Set Sender |
193 | * |
194 | * @param mixed $sender |
195 | * @return AbstractMessage |
196 | */ |
197 | public function setSender(mixed $sender): AbstractMessage |
198 | { |
199 | if ($sender instanceof Value) { |
200 | $sender = (string)$sender; |
201 | } |
202 | $this->addresses['Sender'] = $this->parseAddresses($sender, true); |
203 | return $this->addHeader('Sender', $this->parseAddresses($sender)); |
204 | } |
205 | |
206 | /** |
207 | * Set Return-Path |
208 | * |
209 | * @param mixed $returnPath |
210 | * @return AbstractMessage |
211 | */ |
212 | public function setReturnPath(mixed $returnPath): AbstractMessage |
213 | { |
214 | if ($returnPath instanceof Value) { |
215 | $returnPath = (string)$returnPath; |
216 | } |
217 | $this->addresses['Return-Path'] = $this->parseAddresses($returnPath, true); |
218 | return $this->addHeader('Return-Path', $this->parseAddresses($returnPath)); |
219 | } |
220 | |
221 | /** |
222 | * Set body |
223 | * |
224 | * @param mixed $body |
225 | * @return Message |
226 | */ |
227 | public function setBody(mixed $body): Message |
228 | { |
229 | if (!($body instanceof PartInterface)) { |
230 | $body = new Text($body); |
231 | } |
232 | return $this->addPart($body); |
233 | } |
234 | |
235 | /** |
236 | * Add text message part |
237 | * |
238 | * @param mixed $text |
239 | * @return Message |
240 | */ |
241 | public function addText(mixed $text): Message |
242 | { |
243 | if (!($text instanceof Text) && is_string($text)) { |
244 | $text = new Text($text); |
245 | } |
246 | return $this->addPart($text); |
247 | } |
248 | |
249 | /** |
250 | * Add HTML message part |
251 | * |
252 | * @param mixed $html |
253 | * @return Message |
254 | */ |
255 | public function addHtml(mixed $html): Message |
256 | { |
257 | if (!($html instanceof Html) && is_string($html)) { |
258 | $html = new Html($html); |
259 | } |
260 | return $this->addPart($html); |
261 | } |
262 | |
263 | /** |
264 | * Attach file message part |
265 | * |
266 | * @param string $file |
267 | * @param string $encoding |
268 | * @return Message |
269 | */ |
270 | public function attachFile(string $file, string $encoding = AbstractPart::BASE64): Message |
271 | { |
272 | if (!($file instanceof Attachment)) { |
273 | $options = [ |
274 | 'encoding' => $encoding, |
275 | 'chunk' => true |
276 | ]; |
277 | $file = Attachment::createFromFile($file, $options); |
278 | } |
279 | return $this->addPart($file); |
280 | } |
281 | |
282 | /** |
283 | * Attach file message part from stream |
284 | * |
285 | * @param string $stream |
286 | * @param string $basename |
287 | * @param string $encoding |
288 | * @return Message |
289 | */ |
290 | public function attachFileFromStream( |
291 | string $stream, string $basename = 'file.tmp', string $encoding = AbstractPart::BASE64 |
292 | ): Message |
293 | { |
294 | $options = [ |
295 | 'basename' => $basename, |
296 | 'encoding' => $encoding, |
297 | 'chunk' => true |
298 | ]; |
299 | $file = Attachment::createFromStream($stream, $options); |
300 | return $this->addPart($file); |
301 | } |
302 | |
303 | /** |
304 | * Add message part |
305 | * |
306 | * @param PartInterface $part |
307 | * @return Message |
308 | */ |
309 | public function addPart(PartInterface $part): Message |
310 | { |
311 | $this->parts[] = $part; |
312 | $this->validateContentType(); |
313 | return $this; |
314 | } |
315 | |
316 | /** |
317 | * Remove header |
318 | * |
319 | * @param string $header |
320 | * @return Message |
321 | */ |
322 | public function removeHeader(string $header): Message |
323 | { |
324 | if (isset($this->headers[$header])) { |
325 | unset($this->headers[$header]); |
326 | } |
327 | return $this; |
328 | } |
329 | |
330 | /** |
331 | * Get subject |
332 | * |
333 | * @return ?string |
334 | */ |
335 | public function getSubject(): ?string |
336 | { |
337 | return $this->getHeader('Subject'); |
338 | } |
339 | |
340 | /** |
341 | * Get To |
342 | * |
343 | * @return array |
344 | */ |
345 | public function getTo(): array |
346 | { |
347 | return $this->addresses['To']; |
348 | } |
349 | |
350 | /** |
351 | * Get CC |
352 | * |
353 | * @return array |
354 | */ |
355 | public function getCc(): array |
356 | { |
357 | return $this->addresses['CC']; |
358 | } |
359 | |
360 | /** |
361 | * Get BCC |
362 | * |
363 | * @return array |
364 | */ |
365 | public function getBcc(): array |
366 | { |
367 | return $this->addresses['BCC']; |
368 | } |
369 | |
370 | /** |
371 | * Get From |
372 | * |
373 | * @return array |
374 | */ |
375 | public function getFrom(): array |
376 | { |
377 | return $this->addresses['From']; |
378 | } |
379 | |
380 | /** |
381 | * Get Reply-To |
382 | * |
383 | * @return array |
384 | */ |
385 | public function getReplyTo(): array |
386 | { |
387 | return $this->addresses['Reply-To']; |
388 | } |
389 | |
390 | /** |
391 | * Get Sender |
392 | * |
393 | * @return array |
394 | */ |
395 | public function getSender(): array |
396 | { |
397 | return $this->addresses['Sender']; |
398 | } |
399 | |
400 | /** |
401 | * Get Return-Path |
402 | * |
403 | * @return array |
404 | */ |
405 | public function getReturnPath(): array |
406 | { |
407 | return $this->addresses['Return-Path']; |
408 | } |
409 | |
410 | /** |
411 | * Has To |
412 | * |
413 | * @return bool |
414 | */ |
415 | public function hasTo(): bool |
416 | { |
417 | return !empty($this->addresses['To']); |
418 | } |
419 | |
420 | /** |
421 | * Has CC |
422 | * |
423 | * @return bool |
424 | */ |
425 | public function hasCc(): bool |
426 | { |
427 | return !empty($this->addresses['CC']); |
428 | } |
429 | |
430 | /** |
431 | * Has BCC |
432 | * |
433 | * @return bool |
434 | */ |
435 | public function hasBcc(): bool |
436 | { |
437 | return !empty($this->addresses['BCC']); |
438 | } |
439 | |
440 | /** |
441 | * Has From |
442 | * |
443 | * @return bool |
444 | */ |
445 | public function hasFrom(): bool |
446 | { |
447 | return !empty($this->addresses['From']); |
448 | } |
449 | |
450 | /** |
451 | * Has Reply-To |
452 | * |
453 | * @return bool |
454 | */ |
455 | public function hasReplyTo(): bool |
456 | { |
457 | return !empty($this->addresses['Reply-To']); |
458 | } |
459 | |
460 | /** |
461 | * Has Sender |
462 | * |
463 | * @return bool |
464 | */ |
465 | public function hasSender(): bool |
466 | { |
467 | return !empty($this->addresses['Sender']); |
468 | } |
469 | |
470 | /** |
471 | * Has Return-Path |
472 | * |
473 | * @return bool |
474 | */ |
475 | public function hasReturnPath(): bool |
476 | { |
477 | return !empty($this->addresses['Return-Path']); |
478 | } |
479 | |
480 | /** |
481 | * Has attachments |
482 | * |
483 | * @return bool |
484 | */ |
485 | public function hasAttachments(): bool |
486 | { |
487 | foreach ($this->parts as $part) { |
488 | if ($part instanceof Attachment) { |
489 | return true; |
490 | } |
491 | } |
492 | |
493 | return false; |
494 | } |
495 | |
496 | /** |
497 | * Get message body |
498 | * |
499 | * @return ?string |
500 | */ |
501 | public function getBody(): ?string |
502 | { |
503 | $body = null; |
504 | |
505 | if (count($this->parts) > 1) { |
506 | foreach ($this->parts as $part) { |
507 | $body .= '--' . $this->getBoundary() . self::CRLF . $part . self::CRLF; |
508 | } |
509 | $body .= '--' . $this->getBoundary() . '--'; |
510 | } else if (count($this->parts) == 1) { |
511 | $part = $this->parts[0]; |
512 | if (($part instanceof Text) || ($part instanceof Html)) { |
513 | $body .= $part->getBody() . self::CRLF; |
514 | } else { |
515 | $body .= '--' . $this->getBoundary() . self::CRLF . $part . self::CRLF; |
516 | $body .= '--' . $this->getBoundary() . '--'; |
517 | } |
518 | } |
519 | |
520 | return $body; |
521 | } |
522 | |
523 | /** |
524 | * Get message MIME boundary |
525 | * |
526 | * @return string |
527 | */ |
528 | public function getBoundary(): string |
529 | { |
530 | if ($this->boundary === null) { |
531 | $this->generateBoundary(); |
532 | } |
533 | return $this->boundary; |
534 | } |
535 | |
536 | /** |
537 | * Get message part |
538 | * |
539 | * @param int $i |
540 | * @return PartInterface|null |
541 | */ |
542 | public function getPart(int $i): PartInterface|null |
543 | { |
544 | return $this->parts[$i] ?? null; |
545 | } |
546 | |
547 | /** |
548 | * Get message parts |
549 | * |
550 | * @return array |
551 | */ |
552 | public function getParts(): array |
553 | { |
554 | return $this->parts; |
555 | } |
556 | |
557 | /** |
558 | * Set message MIME boundary |
559 | * |
560 | * @param string $boundary |
561 | * @return Message |
562 | */ |
563 | public function setBoundary(string $boundary): Message |
564 | { |
565 | $this->boundary = $boundary; |
566 | $this->addHeader('MIME-Version', '1.0'); |
567 | return $this; |
568 | } |
569 | |
570 | /** |
571 | * Generate message MIME boundary |
572 | * |
573 | * @return Message |
574 | */ |
575 | public function generateBoundary(): Message |
576 | { |
577 | return $this->setBoundary(sha1(time())); |
578 | } |
579 | |
580 | /** |
581 | * Render message |
582 | * |
583 | * @param array $omitHeaders |
584 | * @return string |
585 | */ |
586 | public function render(array $omitHeaders = []): string |
587 | { |
588 | return $this->getHeadersAsString($omitHeaders) . self::CRLF . $this->getBody(); |
589 | } |
590 | |
591 | /** |
592 | * Save message to file on disk |
593 | * |
594 | * @param string $to |
595 | * @param array $omitHeaders |
596 | * @return void |
597 | */ |
598 | public function save(string $to, array $omitHeaders = []): void |
599 | { |
600 | file_put_contents($to, $this->render($omitHeaders)); |
601 | } |
602 | |
603 | /** |
604 | * Render as an array of lines |
605 | * |
606 | * @param array $omitHeaders |
607 | * @return array |
608 | */ |
609 | public function renderAsLines(array $omitHeaders = []): array |
610 | { |
611 | $lines = []; |
612 | $headers = explode(Message::CRLF, $this->getHeadersAsString($omitHeaders) . Message::CRLF); |
613 | $body = explode(Message::CRLF, $this->getBody()); |
614 | |
615 | foreach ($headers as $header) { |
616 | $lines[] = trim($header); |
617 | } |
618 | |
619 | foreach ($body as $line) { |
620 | $lines[] = trim($line); |
621 | } |
622 | |
623 | return $lines; |
624 | } |
625 | |
626 | /** |
627 | * Write this entire entity to a buffer |
628 | * |
629 | * @param Transport\Smtp\Stream\BufferInterface $is |
630 | * @param array $omitHeaders |
631 | * @return void |
632 | */ |
633 | public function toByteStream(Transport\Smtp\Stream\BufferInterface $is, array $omitHeaders = []): void |
634 | { |
635 | $lines = $this->renderAsLines($omitHeaders); |
636 | foreach ($lines as $line) { |
637 | $is->write($line . self::CRLF); |
638 | } |
639 | $is->commit(); |
640 | } |
641 | |
642 | /** |
643 | * Parse message from file |
644 | * |
645 | * @param string $file |
646 | * @throws Exception |
647 | * @return Message |
648 | */ |
649 | public static function parseFromFile(string $file): Message |
650 | { |
651 | if (!file_exists($file)) { |
652 | throw new Exception("Error: The file '" . $file . "' does not exist."); |
653 | } |
654 | |
655 | return self::parse(file_get_contents($file)); |
656 | } |
657 | |
658 | /** |
659 | * Parse message from string |
660 | * |
661 | * @param string $stream |
662 | * @throws Exception |
663 | * @return Message |
664 | */ |
665 | public static function parse(string $stream): Message |
666 | { |
667 | $parsedMessage = \Pop\Mime\Message::parseMessage($stream); |
668 | $message = new self(); |
669 | |
670 | if ($parsedMessage->hasHeaders()) { |
671 | $headers = $parsedMessage->getHeaders(); |
672 | foreach ($headers as $header => $value) { |
673 | if (count($value->getValues()) == 1) { |
674 | switch (strtolower($header)) { |
675 | case 'subject': |
676 | $message->setSubject($value->getValue(0)); |
677 | break; |
678 | case 'to': |
679 | $message->setTo($value->getValue(0)); |
680 | break; |
681 | case 'cc': |
682 | $message->setCc($value->getValue(0)); |
683 | break; |
684 | case 'bcc': |
685 | $message->setBcc($value->getValue(0)); |
686 | break; |
687 | case 'from': |
688 | $message->setFrom($value->getValue(0)); |
689 | break; |
690 | case 'reply-to': |
691 | $message->setReplyTo($value->getValue(0)); |
692 | break; |
693 | case 'sender': |
694 | $message->setSender($value->getValue(0)); |
695 | break; |
696 | case 'return-path': |
697 | $message->setReturnPath($value->getValue(0)); |
698 | break; |
699 | default: |
700 | $message->addHeader($header, $value->getValue(0)); |
701 | } |
702 | } |
703 | } |
704 | } |
705 | |
706 | if (empty($message->getSubject())) { |
707 | throw new Exception('Error: There is no subject in the message contents'); |
708 | } |
709 | |
710 | if (empty($message->getTo())) { |
711 | throw new Exception('Error: There is no to address in the message contents'); |
712 | } |
713 | |
714 | if ($parsedMessage->hasParts()) { |
715 | $parts = Part::parseParts($parsedMessage->getParts()); |
716 | |
717 | foreach ($parts as $part) { |
718 | if ($part->attachment) { |
719 | $options = [ |
720 | 'contentType' => $part->type, |
721 | 'basename' => $part->basename, |
722 | 'encoding' => AbstractPart::BASE64, |
723 | 'chunk' => true |
724 | ]; |
725 | $message->addPart(Attachment::createFromStream($part->content, $options)); |
726 | } else if (!empty($part->type) && (stripos($part->type, 'html') !== false)) { |
727 | $message->addPart(new Html($part->content)); |
728 | } else if (!empty($part->type) && (stripos($part->type, 'text') !== false)) { |
729 | $message->addPart(new Text($part->content)); |
730 | } else { |
731 | $message->addPart(new Simple($part->content)); |
732 | } |
733 | } |
734 | } |
735 | |
736 | return $message; |
737 | } |
738 | |
739 | /** |
740 | * Decode text |
741 | * |
742 | * @param string $text |
743 | * @return string |
744 | */ |
745 | public static function decodeText(string $text): string |
746 | { |
747 | $decodedValues = imap_mime_header_decode($text); |
748 | $decoded = ''; |
749 | |
750 | foreach ($decodedValues as $string) { |
751 | $decoded .= $string->text; |
752 | } |
753 | |
754 | return $decoded; |
755 | } |
756 | |
757 | /** |
758 | * Parse addresses |
759 | * |
760 | * @param mixed $addresses |
761 | * @param bool $asArray |
762 | * @return string|array |
763 | */ |
764 | public function parseAddresses(mixed $addresses, bool $asArray = false): string|array |
765 | { |
766 | $formatted = []; |
767 | $emails = []; |
768 | |
769 | if (is_array($addresses)) { |
770 | foreach ($addresses as $key => $value) { |
771 | if (($value instanceof \stdClass) && isset($value->mailbox) && isset($value->host)) { |
772 | $formatted[] = $value->mailbox . '@' . $value->host; |
773 | $emails[$value->mailbox . '@' . $value->host] = null; |
774 | } else { |
775 | // $key is email |
776 | if (str_contains($key, '@')) { |
777 | if (!empty($value) && !is_numeric($value)) { |
778 | $formatted[] = '"' . $value . '" <' . $key . '>'; |
779 | $emails[$key] = $value; |
780 | } else { |
781 | $formatted[] = $key; |
782 | $emails[$key] = null; |
783 | } |
784 | // $value is email |
785 | } else if (str_contains($value, '@')) { |
786 | if (!empty($key) && !is_numeric($key)) { |
787 | $formatted[] = '"' . $key . '" <' . $value . '>'; |
788 | $emails[$value] = $key; |
789 | } else { |
790 | $formatted[] = $value; |
791 | $emails[$value] = null; |
792 | } |
793 | } |
794 | } |
795 | } |
796 | } else if (is_string($addresses) && (str_contains($addresses, '@'))) { |
797 | $formatted = [$addresses]; |
798 | if (str_contains($addresses, ',')) { |
799 | $addresses = explode(',', $addresses); |
800 | foreach ($addresses as $address) { |
801 | $address = $this->parseNameAndEmail(trim($address)); |
802 | $emails[$address['email']] = $address['name']; |
803 | } |
804 | } else { |
805 | $address = $this->parseNameAndEmail(trim($addresses)); |
806 | $emails[$address['email']] = $address['name']; |
807 | } |
808 | } |
809 | |
810 | return ($asArray) ? $emails : implode(', ', $formatted); |
811 | } |
812 | |
813 | /** |
814 | * Parse a name and email from an address string |
815 | * |
816 | * @param string $address |
817 | * @return array |
818 | */ |
819 | public function parseNameAndEmail(string $address): array |
820 | { |
821 | $name = null; |
822 | $email = null; |
823 | |
824 | if ((str_contains($address, '<')) && (str_contains($address, '>'))) { |
825 | $name = trim(substr($address, 0, strpos($address, '<'))); |
826 | $email = substr($address, (strpos($address, '<') + 1)); |
827 | $email = trim(substr($email, 0, -1)); |
828 | } else if (str_contains($address, '@')) { |
829 | $email = trim($address); |
830 | } |
831 | |
832 | return ['name' => $name, 'email' => $email]; |
833 | } |
834 | |
835 | /** |
836 | * Validate content type based on message parts added to the message |
837 | * |
838 | * @return void |
839 | */ |
840 | protected function validateContentType(): void |
841 | { |
842 | $hasText = false; |
843 | $hasHtml = false; |
844 | $hasFile = false; |
845 | |
846 | foreach ($this->parts as $part) { |
847 | if ($part instanceof Text) { |
848 | $hasText = true; |
849 | } |
850 | if ($part instanceof Html) { |
851 | $hasHtml = true; |
852 | } |
853 | if ($part instanceof Attachment) { |
854 | $hasFile = true; |
855 | } |
856 | } |
857 | |
858 | if (($hasText) && ($hasHtml)) { |
859 | $this->setContentType( |
860 | 'multipart/alternative; boundary="' . $this->getBoundary() . '"' . self::CRLF . |
861 | 'This is a multi-part message in MIME format.' |
862 | ); |
863 | $this->setCharSet(''); |
864 | } else if ($hasFile) { |
865 | $this->setContentType( |
866 | 'multipart/mixed; boundary="' . $this->getBoundary() . '"' . self::CRLF . |
867 | 'This is a multi-part message in MIME format.' |
868 | ); |
869 | $this->setCharSet(''); |
870 | } |
871 | } |
872 | |
873 | /** |
874 | * Perform a "deep" clone of a message object |
875 | * |
876 | * @return void |
877 | */ |
878 | public function __clone(): void |
879 | { |
880 | foreach($this as $key => $val) { |
881 | if (is_object($val) || (is_array($val))) { |
882 | $this->{$key} = unserialize(serialize($val)); |
883 | } |
884 | } |
885 | } |
886 | |
887 | } |