Message::reset()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 0
dl 0
loc 16
rs 9.8333
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Platine Mail
5
 *
6
 * Platine Mail provides a flexible and powerful PHP email sender
7
 *  with support of SMTP, Native Mail, sendmail, etc transport.
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Mail
12
 * Copyright (c) 2015, Sonia Marquette
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Message.php
35
 *
36
 *  The Message class
37
 *
38
 *  @package    Platine\Mail
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Mail;
50
51
use InvalidArgumentException;
52
53
/**
54
 * @class Message
55
 * @package Platine\Mail
56
 */
57
class Message implements MessageInterface
58
{
59
    /**
60
     * End of line char
61
     */
62
    public const CRLF = PHP_EOL;
63
64
    /**
65
     *
66
     * @var string
67
     */
68
    protected string $from = '';
69
70
    /**
71
     *
72
     * @var string
73
     */
74
    protected string $replyTo = '';
75
76
    /**
77
     * The send mail receiver(s)
78
     * @var array<int, string>
79
     */
80
    protected array $to = [];
81
82
    /**
83
     * The send mail receiver(s) copy
84
     * @var array<int|string, string> $cc
85
     */
86
    protected array $cc = [];
87
88
    /**
89
     * The send mail receiver(s) hidden copy
90
     * @var array<int|string, string> $bcc
91
     */
92
    protected array $bcc = [];
93
94
    /**
95
     * The mail subject
96
     * @var string
97
     */
98
    protected string $subject = '';
99
100
    /**
101
     * The mail body
102
     * @var string
103
     */
104
    protected string $body = '';
105
106
     /**
107
     * The mail attachments
108
     * @var array<int, array<string, string>>
109
     */
110
    protected array $attachments = [];
111
112
     /**
113
     * The mail headers
114
     * @var array<string, mixed>
115
     */
116
    protected array $headers = [];
117
118
    /**
119
     * The mail boundary value
120
     * @var string
121
     */
122
    protected string $uid = '';
123
124
    /**
125
     * Maximum characters for each message line
126
     * @var int
127
     */
128
    protected int $wrap = 70;
129
130
    /**
131
     * Set mail priority
132
     * @var int
133
     */
134
    protected int $priority = 3;
135
136
    /**
137
     * Create new instance
138
     */
139
    public function __construct()
140
    {
141
        $this->reset();
142
    }
143
144
    /**
145
     * {@inheritedoc}
146
     */
147
    public function reset(): self
148
    {
149
        $this->from = '';
150
        $this->replyTo = '';
151
        $this->to = [];
152
        $this->cc = [];
153
        $this->bcc = [];
154
        $this->subject = '';
155
        $this->body = '';
156
        $this->attachments = [];
157
        $this->headers = [];
158
        $this->uid = md5(uniqid((string)time()));
159
        $this->wrap = 70;
160
        $this->priority = 3;
161
162
        return $this;
163
    }
164
165
    /**
166
     * {@inheritedoc}
167
     */
168
    public function addAttachment(string $path, ?string $filename = null): self
169
    {
170
        if (file_exists($path) === false) {
171
            throw new InvalidArgumentException(sprintf(
172
                'The email attachment file [%s] does not exists.',
173
                $path
174
            ));
175
        }
176
177
        if (empty($filename)) {
178
            $filename = basename($path);
179
        }
180
181
        $data = $this->getAttachmentData($path);
182
183
        if ($data !== null) {
184
            $this->attachments[] = [
185
                'file' => $this->encodeUtf8($this->filterString($filename)),
186
                'path' => $path,
187
                'data' => chunk_split(base64_encode($data))
188
            ];
189
        }
190
191
        return $this;
192
    }
193
194
    /**
195
     * {@inheritedoc}
196
     */
197
    public function setBcc(array $pairs): self
198
    {
199
        $this->bcc = $pairs;
200
201
        return $this->addMailHeaders('Bcc', $pairs);
202
    }
203
204
    /**
205
     * {@inheritedoc}
206
     */
207
    public function getBcc(): array
208
    {
209
        return $this->bcc;
210
    }
211
212
    /**
213
     * {@inheritedoc}
214
     */
215
    public function setCc(array $pairs): self
216
    {
217
        $this->cc = $pairs;
218
219
        return $this->addMailHeaders('Cc', $pairs);
220
    }
221
222
    /**
223
     * {@inheritedoc}
224
     */
225
    public function getCc(): array
226
    {
227
        return $this->cc;
228
    }
229
230
    /**
231
     * {@inheritedoc}
232
     */
233
    public function setBody(string $body): self
234
    {
235
        $this->body = str_replace("\n.", "\n..", $body);
236
237
        return $this;
238
    }
239
240
    /**
241
     * {@inheritedoc}
242
     */
243
    public function getEncodedBody(): string
244
    {
245
        $body = wordwrap($this->body, $this->wrap);
246
        if ($this->hasAttachments()) {
247
            $body = $this->getBodyWithAttachments();
248
        }
249
250
        return $body;
251
    }
252
253
    /**
254
     * {@inheritedoc}
255
     */
256
    public function getEncodedHeaders(): string
257
    {
258
        $this->prepareHeaders();
259
260
        $content = '';
261
        foreach ($this->headers as $name => $value) {
262
            $content .= $name . ': ' . $value . self::CRLF;
263
        }
264
265
        return $content;
266
    }
267
268
    /**
269
     * {@inheritedoc}
270
     */
271
    public function setFrom(string $email, ?string $name = null): self
272
    {
273
        $this->from = $this->formatHeader($email, $name);
274
275
        return $this->addMailHeader('From', $email, $name);
276
    }
277
278
    /**
279
     * {@inheritedoc}
280
     */
281
    public function getFrom(): string
282
    {
283
        return $this->from;
284
    }
285
286
    /**
287
    *  {@inheritedoc}
288
    */
289
    public function setReplyTo(string $email, ?string $name = null): self
290
    {
291
        $this->replyTo = $this->formatHeader($email, $name);
292
293
        return $this->addMailHeader('Reply-To', $email, $name);
294
    }
295
296
    /**
297
     * {@inheritedoc}
298
     */
299
    public function getSubject(): string
300
    {
301
        return $this->subject;
302
    }
303
304
    /**
305
     * {@inheritedoc}
306
     */
307
    public function setSubject(string $subject): self
308
    {
309
        $this->subject = $this->encodeUtf8($this->filterString($subject));
310
311
        return $this;
312
    }
313
314
    /**
315
     * {@inheritedoc}
316
     */
317
    public function setTo(string $email, ?string $name = null): self
318
    {
319
        $this->to[] = $this->formatHeader($email, $name);
320
321
        return $this;
322
    }
323
324
    /**
325
     * {@inheritedoc}
326
     */
327
    public function getEncodedTo(): string
328
    {
329
        return join(', ', $this->to);
330
    }
331
332
    /**
333
     * {@inheritedoc}
334
     */
335
    public function getTo(): array
336
    {
337
        return $this->to;
338
    }
339
340
    /**
341
     * {@inheritedoc}
342
     */
343
    public function setWrap(int $wrap = 70): self
344
    {
345
        if ($wrap < 1) {
346
            $wrap = 70;
347
        }
348
349
        $this->wrap = $wrap;
350
351
        return $this;
352
    }
353
354
    /**
355
     * {@inheritedoc}
356
     */
357
    public function setPriority(int $priority = 3): self
358
    {
359
        if ($priority < 1 || $priority > 5) {
360
            $priority = 3;
361
        }
362
363
        $this->priority = $priority;
364
365
        return $this;
366
    }
367
368
    /**
369
     * {@inheritedoc}
370
     */
371
    public function getHeader(string $name, mixed $default = null): mixed
372
    {
373
        $this->prepareHeaders();
374
375
        return array_key_exists($name, $this->headers)
376
                ? $this->headers[$name]
377
                : $default;
378
    }
379
380
    /**
381
     * {@inheritedoc}
382
     */
383
    public function addHeader(string $name, mixed $value): self
384
    {
385
        $this->headers[$name] = $value;
386
387
        return $this;
388
    }
389
390
    /**
391
     * {@inheritedoc}
392
     */
393
    public function addMailHeader(string $header, string $email, ?string $name = null): self
394
    {
395
        $address = $this->formatHeader($email, $name);
396
        $this->headers[$header] = $address;
397
398
        return $this;
399
    }
400
401
    /**
402
     * {@inheritedoc}
403
     */
404
    public function addMailHeaders(string $header, array $pairs): self
405
    {
406
        if (count($pairs) === 0) {
407
            throw new InvalidArgumentException('The mail headers is empty');
408
        }
409
410
        $addresses = [];
411
        foreach ($pairs as $name => $email) {
412
            if (is_numeric($name)) {
413
                $name = null;
414
            }
415
            $addresses[] = $this->formatHeader($email, $name);
416
        }
417
418
        $this->addHeader($header, implode(', ', $addresses));
419
420
        return $this;
421
    }
422
423
    /**
424
     * {@inheritedoc}
425
     */
426
    public function hasAttachments(): bool
427
    {
428
        return !empty($this->attachments);
429
    }
430
431
    /**
432
     * {@inheritedoc}
433
     */
434
    public function setHtml(): self
435
    {
436
        return $this->addHeader('Content-Type', 'text/html; charset="UTF-8"');
437
    }
438
439
    /**
440
     * {@inheritedoc}
441
     */
442
    public function __toString(): string
443
    {
444
        $content = $this->getEncodedHeaders();
445
        $content .= self::CRLF;
446
        $content .= $this->getEncodedBody();
447
448
        return $content;
449
    }
450
451
    /**
452
     * Prepare mail headers
453
     * @return $this
454
     */
455
    protected function prepareHeaders(): self
456
    {
457
        if (!array_key_exists('Return-Path', $this->headers)) {
458
            $this->addHeader('Return-Path', $this->from);
459
        }
460
461
        if (!array_key_exists('Reply-To', $this->headers)) {
462
            $this->addHeader('Reply-To', $this->from);
463
        }
464
465
        $this->addHeader('X-Priority', $this->priority)
466
               ->addHeader('X-Mailer', 'Platine PHP Mail')
467
               ->addHeader('Subject', $this->subject)
468
               ->addHeader('To', join(', ', $this->to))
469
               ->addHeader('Date', date('r'));
470
471
        if ($this->hasAttachments()) {
472
            $this->addHeader('MIME-Version', '1.0')
473
                 ->addHeader(
474
                     'Content-Type',
475
                     sprintf('multipart/mixed; boundary="%s"', $this->uid)
476
                 );
477
        }
478
479
        return $this;
480
    }
481
482
    /**
483
     * Get mail attachment data
484
     * @param string $path
485
     *
486
     * @return string|null
487
     */
488
    protected function getAttachmentData(string $path): ?string
489
    {
490
        $filesize = filesize($path);
491
        if ($filesize === false) {
492
            return null;
493
        }
494
495
        $handle = fopen($path, 'r');
496
        $content = null;
497
        if (is_resource($handle)) {
498
            $result = fread($handle, $filesize);
499
            if ($result !== false) {
500
                $content = $result;
501
            }
502
            fclose($handle);
503
        }
504
505
        return $content;
506
    }
507
508
    /**
509
     * Return the attachment with body
510
     * @return string
511
     */
512
    protected function getBodyWithAttachments(): string
513
    {
514
        $body = [];
515
        $body[] = 'This is a multi-part message in MIME format.';
516
        $body[] = sprintf('--%s', $this->uid);
517
        $body[] = 'Content-Type: text/html; charset="UTF-8"';
518
        $body[] = 'Content-Transfer-Encoding: base64';
519
        $body[] = self::CRLF;
520
        $body[] = chunk_split(base64_encode($this->body));
521
        $body[] = self::CRLF;
522
        $body[] = sprintf('--%s', $this->uid);
523
524
        foreach ($this->attachments as $attachment) {
525
            $body[] = $this->getAttachmentMimeTemplate($attachment);
526
        }
527
528
        return implode(self::CRLF, $body) . '--';
529
    }
530
531
    /**
532
     * Get attachment mime template
533
     * @param array<string, string> $attachment
534
     * @return string
535
     */
536
    protected function getAttachmentMimeTemplate(array $attachment): string
537
    {
538
        $file = $attachment['file'];
539
        $data = $attachment['data'];
540
541
        $head = [];
542
        $head[] = sprintf('Content-Type: application/octet-stream; name="%s"', $file);
543
        $head[] = 'Content-Transfer-Encoding: base64';
544
        $head[] = sprintf('Content-Disposition: attachment; filename="%s"', $file);
545
        $head[] = '';
546
        $head[] = $data;
547
        $head[] = '';
548
        $head[] = sprintf('--%s', $this->uid);
549
550
        return implode(self::CRLF, $head);
551
    }
552
553
    /**
554
     * Format mail header
555
     * @param string $email
556
     * @param string|null $name
557
     * @return string
558
     */
559
    protected function formatHeader(string $email, ?string $name = null): string
560
    {
561
        if (empty($name)) {
562
            return $email;
563
        }
564
565
        return sprintf(
566
            '"%s" <%s>',
567
            $this->encodeUtf8($this->filterName($name)),
568
            $this->filterEmail($email)
569
        );
570
    }
571
572
    /**
573
     * Filter email address
574
     * @param string $email
575
     * @return string
576
     */
577
    protected function filterEmail(string $email): string
578
    {
579
        $rules = [
580
           "\r" => '',
581
            "\n" => '',
582
            "\t" => '',
583
            '"'  => '',
584
            ','  => '',
585
            '<'  => '',
586
            '>'  => ''
587
        ];
588
589
        $emailFiltered = filter_var(
590
            strtr($email, $rules),
591
            FILTER_SANITIZE_EMAIL
592
        );
593
594
        return $emailFiltered === false ? '' : $emailFiltered;
595
    }
596
597
    /**
598
     * Filter name address
599
     * @param string $name
600
     * @return string
601
     */
602
    protected function filterName(string $name): string
603
    {
604
        $rules = [
605
           "\r" => '',
606
            "\n" => '',
607
            "\t" => '',
608
            '"'  => "'",
609
            '<'  => '[',
610
            '>'  => ']',
611
        ];
612
613
        $filtered = filter_var(
614
            $name,
615
            FILTER_SANITIZE_STRING,
616
            FILTER_FLAG_NO_ENCODE_QUOTES
617
        );
618
619
        if ($filtered === false) {
620
            return '';
621
        }
622
623
        return trim(strtr($filtered, $rules));
624
    }
625
626
    /**
627
     * Filter the string other than email and name
628
     * @param string $value
629
     * @return string
630
     */
631
    protected function filterString(string $value): string
632
    {
633
        $filtered = filter_var(
634
            $value,
635
            FILTER_UNSAFE_RAW,
636
            FILTER_FLAG_STRIP_LOW
637
        );
638
        return $filtered === false ? '' : $filtered;
639
    }
640
641
    /**
642
     * Encode the UTF-8 value for the given string
643
     * @param string $value
644
     * @return string
645
     */
646
    protected function encodeUtf8(?string $value): string
647
    {
648
        $valueClean = trim((string)$value);
649
        if (preg_match('/(\s)/', $valueClean)) {
650
            return $this->encodeUtf8Words($valueClean);
651
        }
652
653
        return $this->encodeUtf8Word($valueClean);
654
    }
655
656
    /**
657
     * Encode the UTF-8 value for on word
658
     * @param string $value
659
     * @return string
660
     */
661
    protected function encodeUtf8Word(string $value): string
662
    {
663
        return sprintf('=?UTF-8?B?%s?=', base64_encode($value));
664
    }
665
666
    /**
667
     * Encode the UTF-8 for multiple word
668
     * @param string $value
669
     * @return string
670
     */
671
    protected function encodeUtf8Words(string $value): string
672
    {
673
        $words = explode(' ', $value);
674
        $encoded = [];
675
        foreach ($words as $word) {
676
            $encoded[] = $this->encodeUtf8Word($word);
677
        }
678
        return join($this->encodeUtf8Word(' '), $encoded);
679
    }
680
}
681