Passed
Push — develop ( 58e492...dcbf9f )
by nguereza
02:39
created

Message::getEncodedHeaders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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