Passed
Push — develop ( c34963...58e492 )
by nguereza
02:34
created

Message::getEncodedBody()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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