ci4

Форк
0
/
Email.php 
2274 строки · 58.8 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Email;
15

16
use CodeIgniter\Events\Events;
17
use CodeIgniter\I18n\Time;
18
use Config\Mimes;
19
use ErrorException;
20

21
/**
22
 * CodeIgniter Email Class
23
 *
24
 * Permits email to be sent using Mail, Sendmail, or SMTP.
25
 *
26
 * @see \CodeIgniter\Email\EmailTest
27
 */
28
class Email
29
{
30
    /**
31
     * Properties from the last successful send.
32
     *
33
     * @var array|null
34
     */
35
    public $archive;
36

37
    /**
38
     * Properties to be added to the next archive.
39
     *
40
     * @var array
41
     */
42
    protected $tmpArchive = [];
43

44
    /**
45
     * @var string
46
     */
47
    public $fromEmail;
48

49
    /**
50
     * @var string
51
     */
52
    public $fromName;
53

54
    /**
55
     * Used as the User-Agent and X-Mailer headers' value.
56
     *
57
     * @var string
58
     */
59
    public $userAgent = 'CodeIgniter';
60

61
    /**
62
     * Path to the Sendmail binary.
63
     *
64
     * @var string
65
     */
66
    public $mailPath = '/usr/sbin/sendmail';
67

68
    /**
69
     * Which method to use for sending e-mails.
70
     *
71
     * @var string 'mail', 'sendmail' or 'smtp'
72
     */
73
    public $protocol = 'mail';
74

75
    /**
76
     * STMP Server Hostname
77
     *
78
     * @var string
79
     */
80
    public $SMTPHost = '';
81

82
    /**
83
     * SMTP Username
84
     *
85
     * @var string
86
     */
87
    public $SMTPUser = '';
88

89
    /**
90
     * SMTP Password
91
     *
92
     * @var string
93
     */
94
    public $SMTPPass = '';
95

96
    /**
97
     * SMTP Server port
98
     *
99
     * @var int
100
     */
101
    public $SMTPPort = 25;
102

103
    /**
104
     * SMTP connection timeout in seconds
105
     *
106
     * @var int
107
     */
108
    public $SMTPTimeout = 5;
109

110
    /**
111
     * SMTP persistent connection
112
     *
113
     * @var bool
114
     */
115
    public $SMTPKeepAlive = false;
116

117
    /**
118
     * SMTP Encryption
119
     *
120
     * @var string '', 'tls' or 'ssl'. 'tls' will issue a STARTTLS command
121
     *             to the server. 'ssl' means implicit SSL. Connection on port
122
     *             465 should set this to ''.
123
     */
124
    public $SMTPCrypto = '';
125

126
    /**
127
     * Whether to apply word-wrapping to the message body.
128
     *
129
     * @var bool
130
     */
131
    public $wordWrap = true;
132

133
    /**
134
     * Number of characters to wrap at.
135
     *
136
     * @see Email::$wordWrap
137
     *
138
     * @var int
139
     */
140
    public $wrapChars = 76;
141

142
    /**
143
     * Message format.
144
     *
145
     * @var string 'text' or 'html'
146
     */
147
    public $mailType = 'text';
148

149
    /**
150
     * Character set (default: utf-8)
151
     *
152
     * @var string
153
     */
154
    public $charset = 'UTF-8';
155

156
    /**
157
     * Alternative message (for HTML messages only)
158
     *
159
     * @var string
160
     */
161
    public $altMessage = '';
162

163
    /**
164
     * Whether to validate e-mail addresses.
165
     *
166
     * @var bool
167
     */
168
    public $validate = true;
169

170
    /**
171
     * X-Priority header value.
172
     *
173
     * @var int 1-5
174
     */
175
    public $priority = 3;
176

177
    /**
178
     * Newline character sequence.
179
     * Use "\r\n" to comply with RFC 822.
180
     *
181
     * @see http://www.ietf.org/rfc/rfc822.txt
182
     *
183
     * @var string "\r\n" or "\n"
184
     */
185
    public $newline = "\r\n";
186

187
    /**
188
     * CRLF character sequence
189
     *
190
     * RFC 2045 specifies that for 'quoted-printable' encoding,
191
     * "\r\n" must be used. However, it appears that some servers
192
     * (even on the receiving end) don't handle it properly and
193
     * switching to "\n", while improper, is the only solution
194
     * that seems to work for all environments.
195
     *
196
     * @see http://www.ietf.org/rfc/rfc822.txt
197
     *
198
     * @var string
199
     */
200
    public $CRLF = "\r\n";
201

202
    /**
203
     * Whether to use Delivery Status Notification.
204
     *
205
     * @var bool
206
     */
207
    public $DSN = false;
208

209
    /**
210
     * Whether to send multipart alternatives.
211
     * Yahoo! doesn't seem to like these.
212
     *
213
     * @var bool
214
     */
215
    public $sendMultipart = true;
216

217
    /**
218
     * Whether to send messages to BCC recipients in batches.
219
     *
220
     * @var bool
221
     */
222
    public $BCCBatchMode = false;
223

224
    /**
225
     * BCC Batch max number size.
226
     *
227
     * @see Email::$BCCBatchMode
228
     *
229
     * @var int|string
230
     */
231
    public $BCCBatchSize = 200;
232

233
    /**
234
     * Subject header
235
     *
236
     * @var string
237
     */
238
    protected $subject = '';
239

240
    /**
241
     * Message body
242
     *
243
     * @var string
244
     */
245
    protected $body = '';
246

247
    /**
248
     * Final message body to be sent.
249
     *
250
     * @var string
251
     */
252
    protected $finalBody = '';
253

254
    /**
255
     * Final headers to send
256
     *
257
     * @var string
258
     */
259
    protected $headerStr = '';
260

261
    /**
262
     * SMTP Connection socket placeholder
263
     *
264
     * @var resource|null
265
     */
266
    protected $SMTPConnect;
267

268
    /**
269
     * Mail encoding
270
     *
271
     * @var string '8bit' or '7bit'
272
     */
273
    protected $encoding = '8bit';
274

275
    /**
276
     * Whether to perform SMTP authentication
277
     *
278
     * @var bool
279
     */
280
    protected $SMTPAuth = false;
281

282
    /**
283
     * Whether to send a Reply-To header
284
     *
285
     * @var bool
286
     */
287
    protected $replyToFlag = false;
288

289
    /**
290
     * Debug messages
291
     *
292
     * @see Email::printDebugger()
293
     *
294
     * @var array
295
     */
296
    protected $debugMessage = [];
297

298
    /**
299
     * Raw debug messages
300
     *
301
     * @var list<string>
302
     */
303
    private array $debugMessageRaw = [];
304

305
    /**
306
     * Recipients
307
     *
308
     * @var array|string
309
     */
310
    protected $recipients = [];
311

312
    /**
313
     * CC Recipients
314
     *
315
     * @var array
316
     */
317
    protected $CCArray = [];
318

319
    /**
320
     * BCC Recipients
321
     *
322
     * @var array
323
     */
324
    protected $BCCArray = [];
325

326
    /**
327
     * Message headers
328
     *
329
     * @var array
330
     */
331
    protected $headers = [];
332

333
    /**
334
     * Attachment data
335
     *
336
     * @var array
337
     */
338
    protected $attachments = [];
339

340
    /**
341
     * Valid $protocol values
342
     *
343
     * @see Email::$protocol
344
     *
345
     * @var array
346
     */
347
    protected $protocols = [
348
        'mail',
349
        'sendmail',
350
        'smtp',
351
    ];
352

353
    /**
354
     * Character sets valid for 7-bit encoding,
355
     * excluding language suffix.
356
     *
357
     * @var list<string>
358
     */
359
    protected $baseCharsets = [
360
        'us-ascii',
361
        'iso-2022-',
362
    ];
363

364
    /**
365
     * Bit depths
366
     *
367
     * Valid mail encodings
368
     *
369
     * @see Email::$encoding
370
     *
371
     * @var array
372
     */
373
    protected $bitDepths = [
374
        '7bit',
375
        '8bit',
376
    ];
377

378
    /**
379
     * $priority translations
380
     *
381
     * Actual values to send with the X-Priority header
382
     *
383
     * @var array
384
     */
385
    protected $priorities = [
386
        1 => '1 (Highest)',
387
        2 => '2 (High)',
388
        3 => '3 (Normal)',
389
        4 => '4 (Low)',
390
        5 => '5 (Lowest)',
391
    ];
392

393
    /**
394
     * mbstring.func_overload flag
395
     *
396
     * @var bool
397
     */
398
    protected static $func_overload;
399

400
    /**
401
     * @param array|\Config\Email|null $config
402
     */
403
    public function __construct($config = null)
404
    {
405
        $this->initialize($config);
406
        if (! isset(static::$func_overload)) {
407
            static::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload'));
408
        }
409
    }
410

411
    /**
412
     * Initialize preferences
413
     *
414
     * @param array|\Config\Email|null $config
415
     *
416
     * @return Email
417
     */
418
    public function initialize($config)
419
    {
420
        $this->clear();
421

422
        if ($config instanceof \Config\Email) {
423
            $config = get_object_vars($config);
424
        }
425

426
        foreach (array_keys(get_class_vars(static::class)) as $key) {
427
            if (property_exists($this, $key) && isset($config[$key])) {
428
                $method = 'set' . ucfirst($key);
429

430
                if (method_exists($this, $method)) {
431
                    $this->{$method}($config[$key]);
432
                } else {
433
                    $this->{$key} = $config[$key];
434
                }
435
            }
436
        }
437

438
        $this->charset  = strtoupper($this->charset);
439
        $this->SMTPAuth = isset($this->SMTPUser[0], $this->SMTPPass[0]);
440

441
        return $this;
442
    }
443

444
    /**
445
     * @param bool $clearAttachments
446
     *
447
     * @return Email
448
     */
449
    public function clear($clearAttachments = false)
450
    {
451
        $this->subject         = '';
452
        $this->body            = '';
453
        $this->finalBody       = '';
454
        $this->headerStr       = '';
455
        $this->replyToFlag     = false;
456
        $this->recipients      = [];
457
        $this->CCArray         = [];
458
        $this->BCCArray        = [];
459
        $this->headers         = [];
460
        $this->debugMessage    = [];
461
        $this->debugMessageRaw = [];
462

463
        $this->setHeader('Date', $this->setDate());
464

465
        if ($clearAttachments !== false) {
466
            $this->attachments = [];
467
        }
468

469
        return $this;
470
    }
471

472
    /**
473
     * @param string      $from
474
     * @param string      $name
475
     * @param string|null $returnPath Return-Path
476
     *
477
     * @return Email
478
     */
479
    public function setFrom($from, $name = '', $returnPath = null)
480
    {
481
        if (preg_match('/\<(.*)\>/', $from, $match)) {
482
            $from = $match[1];
483
        }
484

485
        if ($this->validate) {
486
            $this->validateEmail($this->stringToArray($from));
487

488
            if ($returnPath) {
489
                $this->validateEmail($this->stringToArray($returnPath));
490
            }
491
        }
492

493
        $this->tmpArchive['fromEmail'] = $from;
494
        $this->tmpArchive['fromName']  = $name;
495

496
        if ($name !== '') {
497
            // only use Q encoding if there are characters that would require it
498
            if (! preg_match('/[\200-\377]/', $name)) {
499
                $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"';
500
            } else {
501
                $name = $this->prepQEncoding($name);
502
            }
503
        }
504

505
        $this->setHeader('From', $name . ' <' . $from . '>');
506
        if (! isset($returnPath)) {
507
            $returnPath = $from;
508
        }
509
        $this->setHeader('Return-Path', '<' . $returnPath . '>');
510
        $this->tmpArchive['returnPath'] = $returnPath;
511

512
        return $this;
513
    }
514

515
    /**
516
     * @param string $replyto
517
     * @param string $name
518
     *
519
     * @return Email
520
     */
521
    public function setReplyTo($replyto, $name = '')
522
    {
523
        if (preg_match('/\<(.*)\>/', $replyto, $match)) {
524
            $replyto = $match[1];
525
        }
526

527
        if ($this->validate) {
528
            $this->validateEmail($this->stringToArray($replyto));
529
        }
530

531
        if ($name !== '') {
532
            $this->tmpArchive['replyName'] = $name;
533

534
            // only use Q encoding if there are characters that would require it
535
            if (! preg_match('/[\200-\377]/', $name)) {
536
                $name = '"' . addcslashes($name, "\0..\37\177'\"\\") . '"';
537
            } else {
538
                $name = $this->prepQEncoding($name);
539
            }
540
        }
541

542
        $this->setHeader('Reply-To', $name . ' <' . $replyto . '>');
543
        $this->replyToFlag           = true;
544
        $this->tmpArchive['replyTo'] = $replyto;
545

546
        return $this;
547
    }
548

549
    /**
550
     * @param array|string $to
551
     *
552
     * @return Email
553
     */
554
    public function setTo($to)
555
    {
556
        $to = $this->stringToArray($to);
557
        $to = $this->cleanEmail($to);
558

559
        if ($this->validate) {
560
            $this->validateEmail($to);
561
        }
562

563
        if ($this->getProtocol() !== 'mail') {
564
            $this->setHeader('To', implode(', ', $to));
565
        }
566

567
        $this->recipients = $to;
568

569
        return $this;
570
    }
571

572
    /**
573
     * @param string $cc
574
     *
575
     * @return Email
576
     */
577
    public function setCC($cc)
578
    {
579
        $cc = $this->cleanEmail($this->stringToArray($cc));
580

581
        if ($this->validate) {
582
            $this->validateEmail($cc);
583
        }
584

585
        $this->setHeader('Cc', implode(', ', $cc));
586

587
        if ($this->getProtocol() === 'smtp') {
588
            $this->CCArray = $cc;
589
        }
590

591
        $this->tmpArchive['CCArray'] = $cc;
592

593
        return $this;
594
    }
595

596
    /**
597
     * @param string $bcc
598
     * @param string $limit
599
     *
600
     * @return Email
601
     */
602
    public function setBCC($bcc, $limit = '')
603
    {
604
        if ($limit !== '' && is_numeric($limit)) {
605
            $this->BCCBatchMode = true;
606
            $this->BCCBatchSize = $limit;
607
        }
608

609
        $bcc = $this->cleanEmail($this->stringToArray($bcc));
610

611
        if ($this->validate) {
612
            $this->validateEmail($bcc);
613
        }
614

615
        if ($this->getProtocol() === 'smtp' || ($this->BCCBatchMode && count($bcc) > $this->BCCBatchSize)) {
616
            $this->BCCArray = $bcc;
617
        } else {
618
            $this->setHeader('Bcc', implode(', ', $bcc));
619
            $this->tmpArchive['BCCArray'] = $bcc;
620
        }
621

622
        return $this;
623
    }
624

625
    /**
626
     * @param string $subject
627
     *
628
     * @return Email
629
     */
630
    public function setSubject($subject)
631
    {
632
        $this->tmpArchive['subject'] = $subject;
633

634
        $subject = $this->prepQEncoding($subject);
635
        $this->setHeader('Subject', $subject);
636

637
        return $this;
638
    }
639

640
    /**
641
     * @param string $body
642
     *
643
     * @return Email
644
     */
645
    public function setMessage($body)
646
    {
647
        $this->body = rtrim(str_replace("\r", '', $body));
648

649
        return $this;
650
    }
651

652
    /**
653
     * @param string      $file        Can be local path, URL or buffered content
654
     * @param string      $disposition 'attachment'
655
     * @param string|null $newname
656
     * @param string      $mime
657
     *
658
     * @return bool|Email
659
     */
660
    public function attach($file, $disposition = '', $newname = null, $mime = '')
661
    {
662
        if ($mime === '') {
663
            if (! str_contains($file, '://') && ! is_file($file)) {
664
                $this->setErrorMessage(lang('Email.attachmentMissing', [$file]));
665

666
                return false;
667
            }
668

669
            if (! $fp = @fopen($file, 'rb')) {
670
                $this->setErrorMessage(lang('Email.attachmentUnreadable', [$file]));
671

672
                return false;
673
            }
674

675
            $fileContent = stream_get_contents($fp);
676

677
            $mime = $this->mimeTypes(pathinfo($file, PATHINFO_EXTENSION));
678

679
            fclose($fp);
680
        } else {
681
            $fileContent = &$file; // buffered file
682
        }
683

684
        // declare names on their own, to make phpcbf happy
685
        $namesAttached = [$file, $newname];
686

687
        $this->attachments[] = [
688
            'name'        => $namesAttached,
689
            'disposition' => empty($disposition) ? 'attachment' : $disposition,
690
            // Can also be 'inline'  Not sure if it matters
691
            'type'      => $mime,
692
            'content'   => chunk_split(base64_encode($fileContent)),
693
            'multipart' => 'mixed',
694
        ];
695

696
        return $this;
697
    }
698

699
    /**
700
     * Set and return attachment Content-ID
701
     * Useful for attached inline pictures
702
     *
703
     * @param string $filename
704
     *
705
     * @return bool|string
706
     */
707
    public function setAttachmentCID($filename)
708
    {
709
        foreach ($this->attachments as $i => $attachment) {
710
            // For file path.
711
            if ($attachment['name'][0] === $filename) {
712
                $this->attachments[$i]['multipart'] = 'related';
713

714
                $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][0]) . '@', true);
715

716
                return $this->attachments[$i]['cid'];
717
            }
718

719
            // For buffer string.
720
            if ($attachment['name'][1] === $filename) {
721
                $this->attachments[$i]['multipart'] = 'related';
722

723
                $this->attachments[$i]['cid'] = uniqid(basename($attachment['name'][1]) . '@', true);
724

725
                return $this->attachments[$i]['cid'];
726
            }
727
        }
728

729
        return false;
730
    }
731

732
    /**
733
     * @param string $header
734
     * @param string $value
735
     *
736
     * @return Email
737
     */
738
    public function setHeader($header, $value)
739
    {
740
        $this->headers[$header] = str_replace(["\n", "\r"], '', $value);
741

742
        return $this;
743
    }
744

745
    /**
746
     * @param array|string $email
747
     *
748
     * @return array
749
     */
750
    protected function stringToArray($email)
751
    {
752
        if (! is_array($email)) {
753
            return (str_contains($email, ',')) ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) : (array) trim($email);
754
        }
755

756
        return $email;
757
    }
758

759
    /**
760
     * @param string $str
761
     *
762
     * @return Email
763
     */
764
    public function setAltMessage($str)
765
    {
766
        $this->altMessage = (string) $str;
767

768
        return $this;
769
    }
770

771
    /**
772
     * @param string $type
773
     *
774
     * @return Email
775
     */
776
    public function setMailType($type = 'text')
777
    {
778
        $this->mailType = ($type === 'html') ? 'html' : 'text';
779

780
        return $this;
781
    }
782

783
    /**
784
     * @param bool $wordWrap
785
     *
786
     * @return Email
787
     */
788
    public function setWordWrap($wordWrap = true)
789
    {
790
        $this->wordWrap = (bool) $wordWrap;
791

792
        return $this;
793
    }
794

795
    /**
796
     * @param string $protocol
797
     *
798
     * @return Email
799
     */
800
    public function setProtocol($protocol = 'mail')
801
    {
802
        $this->protocol = in_array($protocol, $this->protocols, true) ? strtolower($protocol) : 'mail';
803

804
        return $this;
805
    }
806

807
    /**
808
     * @param int $n
809
     *
810
     * @return Email
811
     */
812
    public function setPriority($n = 3)
813
    {
814
        $this->priority = preg_match('/^[1-5]$/', (string) $n) ? (int) $n : 3;
815

816
        return $this;
817
    }
818

819
    /**
820
     * @param string $newline
821
     *
822
     * @return Email
823
     */
824
    public function setNewline($newline = "\n")
825
    {
826
        $this->newline = in_array($newline, ["\n", "\r\n", "\r"], true) ? $newline : "\n";
827

828
        return $this;
829
    }
830

831
    /**
832
     * @param string $CRLF
833
     *
834
     * @return Email
835
     */
836
    public function setCRLF($CRLF = "\n")
837
    {
838
        $this->CRLF = ($CRLF !== "\n" && $CRLF !== "\r\n" && $CRLF !== "\r") ? "\n" : $CRLF;
839

840
        return $this;
841
    }
842

843
    /**
844
     * @return string
845
     */
846
    protected function getMessageID()
847
    {
848
        $from = str_replace(['>', '<'], '', $this->headers['Return-Path']);
849

850
        return '<' . uniqid('', true) . strstr($from, '@') . '>';
851
    }
852

853
    /**
854
     * @return string
855
     */
856
    protected function getProtocol()
857
    {
858
        $this->protocol = strtolower($this->protocol);
859

860
        if (! in_array($this->protocol, $this->protocols, true)) {
861
            $this->protocol = 'mail';
862
        }
863

864
        return $this->protocol;
865
    }
866

867
    /**
868
     * @return string
869
     */
870
    protected function getEncoding()
871
    {
872
        if (! in_array($this->encoding, $this->bitDepths, true)) {
873
            $this->encoding = '8bit';
874
        }
875

876
        foreach ($this->baseCharsets as $charset) {
877
            if (str_starts_with($this->charset, $charset)) {
878
                $this->encoding = '7bit';
879

880
                break;
881
            }
882
        }
883

884
        return $this->encoding;
885
    }
886

887
    /**
888
     * @return string
889
     */
890
    protected function getContentType()
891
    {
892
        if ($this->mailType === 'html') {
893
            return empty($this->attachments) ? 'html' : 'html-attach';
894
        }
895

896
        if ($this->mailType === 'text' && ! empty($this->attachments)) {
897
            return 'plain-attach';
898
        }
899

900
        return 'plain';
901
    }
902

903
    /**
904
     * Set RFC 822 Date
905
     *
906
     * @return string
907
     */
908
    protected function setDate()
909
    {
910
        $timezone = date('Z');
911
        $operator = ($timezone[0] === '-') ? '-' : '+';
912
        $timezone = abs((int) $timezone);
913
        $timezone = floor($timezone / 3600) * 100 + ($timezone % 3600) / 60;
914

915
        return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone);
916
    }
917

918
    /**
919
     * @return string
920
     */
921
    protected function getMimeMessage()
922
    {
923
        return 'This is a multi-part message in MIME format.' . $this->newline . 'Your email application may not support this format.';
924
    }
925

926
    /**
927
     * @param array|string $email
928
     *
929
     * @return bool
930
     */
931
    public function validateEmail($email)
932
    {
933
        if (! is_array($email)) {
934
            $this->setErrorMessage(lang('Email.mustBeArray'));
935

936
            return false;
937
        }
938

939
        foreach ($email as $val) {
940
            if (! $this->isValidEmail($val)) {
941
                $this->setErrorMessage(lang('Email.invalidAddress', [$val]));
942

943
                return false;
944
            }
945
        }
946

947
        return true;
948
    }
949

950
    /**
951
     * @param string $email
952
     *
953
     * @return bool
954
     */
955
    public function isValidEmail($email)
956
    {
957
        if (function_exists('idn_to_ascii') && defined('INTL_IDNA_VARIANT_UTS46') && $atpos = strpos($email, '@')) {
958
            $email = static::substr($email, 0, ++$atpos)
959
                . idn_to_ascii(static::substr($email, $atpos), 0, INTL_IDNA_VARIANT_UTS46);
960
        }
961

962
        return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
963
    }
964

965
    /**
966
     * @param array|string $email
967
     *
968
     * @return array|string
969
     */
970
    public function cleanEmail($email)
971
    {
972
        if (! is_array($email)) {
973
            return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email;
974
        }
975

976
        $cleanEmail = [];
977

978
        foreach ($email as $addy) {
979
            $cleanEmail[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy;
980
        }
981

982
        return $cleanEmail;
983
    }
984

985
    /**
986
     * Build alternative plain text message
987
     *
988
     * Provides the raw message for use in plain-text headers of
989
     * HTML-formatted emails.
990
     *
991
     * If the user hasn't specified his own alternative message
992
     * it creates one by stripping the HTML
993
     *
994
     * @return string
995
     */
996
    protected function getAltMessage()
997
    {
998
        if (! empty($this->altMessage)) {
999
            return ($this->wordWrap) ? $this->wordWrap($this->altMessage, 76) : $this->altMessage;
1000
        }
1001

1002
        $body = preg_match('/\<body.*?\>(.*)\<\/body\>/si', $this->body, $match) ? $match[1] : $this->body;
1003
        $body = str_replace("\t", '', preg_replace('#<!--(.*)--\>#', '', trim(strip_tags($body))));
1004

1005
        for ($i = 20; $i >= 3; $i--) {
1006
            $body = str_replace(str_repeat("\n", $i), "\n\n", $body);
1007
        }
1008

1009
        $body = preg_replace('| +|', ' ', $body);
1010

1011
        return ($this->wordWrap) ? $this->wordWrap($body, 76) : $body;
1012
    }
1013

1014
    /**
1015
     * @param string   $str
1016
     * @param int|null $charlim Line-length limit
1017
     *
1018
     * @return string
1019
     */
1020
    public function wordWrap($str, $charlim = null)
1021
    {
1022
        if (empty($charlim)) {
1023
            $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars;
1024
        }
1025

1026
        if (str_contains($str, "\r")) {
1027
            $str = str_replace(["\r\n", "\r"], "\n", $str);
1028
        }
1029

1030
        $str = preg_replace('| +\n|', "\n", $str);
1031

1032
        $unwrap = [];
1033

1034
        if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches)) {
1035
            for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
1036
                $unwrap[] = $matches[1][$i];
1037
                $str      = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
1038
            }
1039
        }
1040

1041
        // Use PHP's native function to do the initial wordwrap.
1042
        // We set the cut flag to FALSE so that any individual words that are
1043
        // too long get left alone. In the next step we'll deal with them.
1044
        $str = wordwrap($str, $charlim, "\n", false);
1045

1046
        // Split the string into individual lines of text and cycle through them
1047
        $output = '';
1048

1049
        foreach (explode("\n", $str) as $line) {
1050
            if (static::strlen($line) <= $charlim) {
1051
                $output .= $line . $this->newline;
1052

1053
                continue;
1054
            }
1055

1056
            $temp = '';
1057

1058
            do {
1059
                if (preg_match('!\[url.+\]|://|www\.!', $line)) {
1060
                    break;
1061
                }
1062

1063
                $temp .= static::substr($line, 0, $charlim - 1);
1064
                $line = static::substr($line, $charlim - 1);
1065
            } while (static::strlen($line) > $charlim);
1066

1067
            if ($temp !== '') {
1068
                $output .= $temp . $this->newline;
1069
            }
1070

1071
            $output .= $line . $this->newline;
1072
        }
1073

1074
        foreach ($unwrap as $key => $val) {
1075
            $output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
1076
        }
1077

1078
        return $output;
1079
    }
1080

1081
    /**
1082
     * Build final headers
1083
     *
1084
     * @return void
1085
     */
1086
    protected function buildHeaders()
1087
    {
1088
        $this->setHeader('User-Agent', $this->userAgent);
1089
        $this->setHeader('X-Sender', $this->cleanEmail($this->headers['From']));
1090
        $this->setHeader('X-Mailer', $this->userAgent);
1091
        $this->setHeader('X-Priority', $this->priorities[$this->priority]);
1092
        $this->setHeader('Message-ID', $this->getMessageID());
1093
        $this->setHeader('Mime-Version', '1.0');
1094
    }
1095

1096
    /**
1097
     * Write Headers as a string
1098
     *
1099
     * @return void
1100
     */
1101
    protected function writeHeaders()
1102
    {
1103
        if ($this->protocol === 'mail' && isset($this->headers['Subject'])) {
1104
            $this->subject = $this->headers['Subject'];
1105
            unset($this->headers['Subject']);
1106
        }
1107

1108
        reset($this->headers);
1109
        $this->headerStr = '';
1110

1111
        foreach ($this->headers as $key => $val) {
1112
            $val = trim($val);
1113

1114
            if ($val !== '') {
1115
                $this->headerStr .= $key . ': ' . $val . $this->newline;
1116
            }
1117
        }
1118

1119
        if ($this->getProtocol() === 'mail') {
1120
            $this->headerStr = rtrim($this->headerStr);
1121
        }
1122
    }
1123

1124
    /**
1125
     * Build Final Body and attachments
1126
     *
1127
     * @return void
1128
     */
1129
    protected function buildMessage()
1130
    {
1131
        if ($this->wordWrap === true && $this->mailType !== 'html') {
1132
            $this->body = $this->wordWrap($this->body);
1133
        }
1134

1135
        $this->writeHeaders();
1136
        $hdr  = ($this->getProtocol() === 'mail') ? $this->newline : '';
1137
        $body = '';
1138

1139
        switch ($this->getContentType()) {
1140
            case 'plain':
1141
                $hdr .= 'Content-Type: text/plain; charset='
1142
                    . $this->charset
1143
                    . $this->newline
1144
                    . 'Content-Transfer-Encoding: '
1145
                    . $this->getEncoding();
1146

1147
                if ($this->getProtocol() === 'mail') {
1148
                    $this->headerStr .= $hdr;
1149
                    $this->finalBody = $this->body;
1150
                } else {
1151
                    $this->finalBody = $hdr . $this->newline . $this->newline . $this->body;
1152
                }
1153

1154
                return;
1155

1156
            case 'html':
1157
                $boundary = uniqid('B_ALT_', true);
1158

1159
                if ($this->sendMultipart === false) {
1160
                    $hdr .= 'Content-Type: text/html; charset='
1161
                        . $this->charset . $this->newline
1162
                        . 'Content-Transfer-Encoding: quoted-printable';
1163
                } else {
1164
                    $hdr  .= 'Content-Type: multipart/alternative; boundary="' . $boundary . '"';
1165
                    $body .= $this->getMimeMessage() . $this->newline . $this->newline
1166
                        . '--' . $boundary . $this->newline
1167
                        . 'Content-Type: text/plain; charset=' . $this->charset . $this->newline
1168
                        . 'Content-Transfer-Encoding: ' . $this->getEncoding() . $this->newline . $this->newline
1169
                        . $this->getAltMessage() . $this->newline . $this->newline
1170
                        . '--' . $boundary . $this->newline
1171
                        . 'Content-Type: text/html; charset=' . $this->charset . $this->newline
1172
                        . 'Content-Transfer-Encoding: quoted-printable' . $this->newline . $this->newline;
1173
                }
1174

1175
                $this->finalBody = $body . $this->prepQuotedPrintable($this->body) . $this->newline . $this->newline;
1176

1177
                if ($this->getProtocol() === 'mail') {
1178
                    $this->headerStr .= $hdr;
1179
                } else {
1180
                    $this->finalBody = $hdr . $this->newline . $this->newline . $this->finalBody;
1181
                }
1182

1183
                if ($this->sendMultipart !== false) {
1184
                    $this->finalBody .= '--' . $boundary . '--';
1185
                }
1186

1187
                return;
1188

1189
            case 'plain-attach':
1190
                $boundary = uniqid('B_ATC_', true);
1191
                $hdr .= 'Content-Type: multipart/mixed; boundary="' . $boundary . '"';
1192

1193
                if ($this->getProtocol() === 'mail') {
1194
                    $this->headerStr .= $hdr;
1195
                }
1196

1197
                $body .= $this->getMimeMessage() . $this->newline
1198
                    . $this->newline
1199
                    . '--' . $boundary . $this->newline
1200
                    . 'Content-Type: text/plain; charset=' . $this->charset . $this->newline
1201
                    . 'Content-Transfer-Encoding: ' . $this->getEncoding() . $this->newline
1202
                    . $this->newline
1203
                    . $this->body . $this->newline . $this->newline;
1204

1205
                $this->appendAttachments($body, $boundary);
1206
                break;
1207

1208
            case 'html-attach':
1209
                $altBoundary  = uniqid('B_ALT_', true);
1210
                $lastBoundary = null;
1211

1212
                if ($this->attachmentsHaveMultipart('mixed')) {
1213
                    $atcBoundary = uniqid('B_ATC_', true);
1214
                    $hdr .= 'Content-Type: multipart/mixed; boundary="' . $atcBoundary . '"';
1215
                    $lastBoundary = $atcBoundary;
1216
                }
1217

1218
                if ($this->attachmentsHaveMultipart('related')) {
1219
                    $relBoundary = uniqid('B_REL_', true);
1220

1221
                    $relBoundaryHeader = 'Content-Type: multipart/related; boundary="' . $relBoundary . '"';
1222

1223
                    if (isset($lastBoundary)) {
1224
                        $body .= '--' . $lastBoundary . $this->newline . $relBoundaryHeader;
1225
                    } else {
1226
                        $hdr .= $relBoundaryHeader;
1227
                    }
1228

1229
                    $lastBoundary = $relBoundary;
1230
                }
1231

1232
                if ($this->getProtocol() === 'mail') {
1233
                    $this->headerStr .= $hdr;
1234
                }
1235

1236
                static::strlen($body) && $body .= $this->newline . $this->newline;
1237

1238
                $body .= $this->getMimeMessage() . $this->newline . $this->newline
1239
                    . '--' . $lastBoundary . $this->newline
1240
                    . 'Content-Type: multipart/alternative; boundary="' . $altBoundary . '"' . $this->newline . $this->newline
1241
                    . '--' . $altBoundary . $this->newline
1242
                    . 'Content-Type: text/plain; charset=' . $this->charset . $this->newline
1243
                    . 'Content-Transfer-Encoding: ' . $this->getEncoding() . $this->newline . $this->newline
1244
                    . $this->getAltMessage() . $this->newline . $this->newline
1245
                    . '--' . $altBoundary . $this->newline
1246
                    . 'Content-Type: text/html; charset=' . $this->charset . $this->newline
1247
                    . 'Content-Transfer-Encoding: quoted-printable' . $this->newline . $this->newline
1248
                    . $this->prepQuotedPrintable($this->body) . $this->newline . $this->newline
1249
                    . '--' . $altBoundary . '--' . $this->newline . $this->newline;
1250

1251
                if (isset($relBoundary)) {
1252
                    $body .= $this->newline . $this->newline;
1253
                    $this->appendAttachments($body, $relBoundary, 'related');
1254
                }
1255

1256
                // multipart/mixed attachments
1257
                if (isset($atcBoundary)) {
1258
                    $body .= $this->newline . $this->newline;
1259
                    $this->appendAttachments($body, $atcBoundary, 'mixed');
1260
                }
1261

1262
                break;
1263
        }
1264

1265
        $this->finalBody = ($this->getProtocol() === 'mail') ? $body : $hdr . $this->newline . $this->newline . $body;
1266
    }
1267

1268
    /**
1269
     * @param mixed $type
1270
     *
1271
     * @return bool
1272
     */
1273
    protected function attachmentsHaveMultipart($type)
1274
    {
1275
        foreach ($this->attachments as &$attachment) {
1276
            if ($attachment['multipart'] === $type) {
1277
                return true;
1278
            }
1279
        }
1280

1281
        return false;
1282
    }
1283

1284
    /**
1285
     * @param string      $body      Message body to append to
1286
     * @param string      $boundary  Multipart boundary
1287
     * @param string|null $multipart When provided, only attachments of this type will be processed
1288
     *
1289
     * @return void
1290
     */
1291
    protected function appendAttachments(&$body, $boundary, $multipart = null)
1292
    {
1293
        foreach ($this->attachments as $attachment) {
1294
            if (isset($multipart) && $attachment['multipart'] !== $multipart) {
1295
                continue;
1296
            }
1297

1298
            $name = $attachment['name'][1] ?? basename($attachment['name'][0]);
1299
            $body .= '--' . $boundary . $this->newline
1300
                . 'Content-Type: ' . $attachment['type'] . '; name="' . $name . '"' . $this->newline
1301
                . 'Content-Disposition: ' . $attachment['disposition'] . ';' . $this->newline
1302
                . 'Content-Transfer-Encoding: base64' . $this->newline
1303
                . (empty($attachment['cid']) ? '' : 'Content-ID: <' . $attachment['cid'] . '>' . $this->newline)
1304
                . $this->newline
1305
                . $attachment['content'] . $this->newline;
1306
        }
1307

1308
        // $name won't be set if no attachments were appended,
1309
        // and therefore a boundary wouldn't be necessary
1310
        if (! empty($name)) {
1311
            $body .= '--' . $boundary . '--';
1312
        }
1313
    }
1314

1315
    /**
1316
     * Prepares string for Quoted-Printable Content-Transfer-Encoding
1317
     * Refer to RFC 2045 http://www.ietf.org/rfc/rfc2045.txt
1318
     *
1319
     * @param string $str
1320
     *
1321
     * @return string
1322
     */
1323
    protected function prepQuotedPrintable($str)
1324
    {
1325
        // ASCII code numbers for "safe" characters that can always be
1326
        // used literally, without encoding, as described in RFC 2049.
1327
        // http://www.ietf.org/rfc/rfc2049.txt
1328
        static $asciiSafeChars = [
1329
            // ' (  )   +   ,   -   .   /   :   =   ?
1330
            39,
1331
            40,
1332
            41,
1333
            43,
1334
            44,
1335
            45,
1336
            46,
1337
            47,
1338
            58,
1339
            61,
1340
            63,
1341
            // numbers
1342
            48,
1343
            49,
1344
            50,
1345
            51,
1346
            52,
1347
            53,
1348
            54,
1349
            55,
1350
            56,
1351
            57,
1352
            // upper-case letters
1353
            65,
1354
            66,
1355
            67,
1356
            68,
1357
            69,
1358
            70,
1359
            71,
1360
            72,
1361
            73,
1362
            74,
1363
            75,
1364
            76,
1365
            77,
1366
            78,
1367
            79,
1368
            80,
1369
            81,
1370
            82,
1371
            83,
1372
            84,
1373
            85,
1374
            86,
1375
            87,
1376
            88,
1377
            89,
1378
            90,
1379
            // lower-case letters
1380
            97,
1381
            98,
1382
            99,
1383
            100,
1384
            101,
1385
            102,
1386
            103,
1387
            104,
1388
            105,
1389
            106,
1390
            107,
1391
            108,
1392
            109,
1393
            110,
1394
            111,
1395
            112,
1396
            113,
1397
            114,
1398
            115,
1399
            116,
1400
            117,
1401
            118,
1402
            119,
1403
            120,
1404
            121,
1405
            122,
1406
        ];
1407

1408
        // We are intentionally wrapping so mail servers will encode characters
1409
        // properly and MUAs will behave, so {unwrap} must go!
1410
        $str = str_replace(['{unwrap}', '{/unwrap}'], '', $str);
1411

1412
        // RFC 2045 specifies CRLF as "\r\n".
1413
        // However, many developers choose to override that and violate
1414
        // the RFC rules due to (apparently) a bug in MS Exchange,
1415
        // which only works with "\n".
1416
        if ($this->CRLF === "\r\n") {
1417
            return quoted_printable_encode($str);
1418
        }
1419

1420
        // Reduce multiple spaces & remove nulls
1421
        $str = preg_replace(['| +|', '/\x00+/'], [' ', ''], $str);
1422

1423
        // Standardize newlines
1424
        if (str_contains($str, "\r")) {
1425
            $str = str_replace(["\r\n", "\r"], "\n", $str);
1426
        }
1427

1428
        $escape = '=';
1429
        $output = '';
1430

1431
        foreach (explode("\n", $str) as $line) {
1432
            $length = static::strlen($line);
1433
            $temp   = '';
1434

1435
            // Loop through each character in the line to add soft-wrap
1436
            // characters at the end of a line " =\r\n" and add the newly
1437
            // processed line(s) to the output (see comment on $crlf class property)
1438
            for ($i = 0; $i < $length; $i++) {
1439
                // Grab the next character
1440
                $char  = $line[$i];
1441
                $ascii = ord($char);
1442

1443
                // Convert spaces and tabs but only if it's the end of the line
1444
                if ($ascii === 32 || $ascii === 9) {
1445
                    if ($i === ($length - 1)) {
1446
                        $char = $escape . sprintf('%02s', dechex($ascii));
1447
                    }
1448
                }
1449
                // DO NOT move this below the $ascii_safe_chars line!
1450
                //
1451
                // = (equals) signs are allowed by RFC2049, but must be encoded
1452
                // as they are the encoding delimiter!
1453
                elseif ($ascii === 61) {
1454
                    $char = $escape . strtoupper(sprintf('%02s', dechex($ascii)));  // =3D
1455
                } elseif (! in_array($ascii, $asciiSafeChars, true)) {
1456
                    $char = $escape . strtoupper(sprintf('%02s', dechex($ascii)));
1457
                }
1458

1459
                // If we're at the character limit, add the line to the output,
1460
                // reset our temp variable, and keep on chuggin'
1461
                if ((static::strlen($temp) + static::strlen($char)) >= 76) {
1462
                    $output .= $temp . $escape . $this->CRLF;
1463
                    $temp = '';
1464
                }
1465

1466
                // Add the character to our temporary line
1467
                $temp .= $char;
1468
            }
1469

1470
            // Add our completed line to the output
1471
            $output .= $temp . $this->CRLF;
1472
        }
1473

1474
        // get rid of extra CRLF tacked onto the end
1475
        return static::substr($output, 0, static::strlen($this->CRLF) * -1);
1476
    }
1477

1478
    /**
1479
     * Performs "Q Encoding" on a string for use in email headers.
1480
     * It's related but not identical to quoted-printable, so it has its
1481
     * own method.
1482
     *
1483
     * @param string $str
1484
     *
1485
     * @return string
1486
     */
1487
    protected function prepQEncoding($str)
1488
    {
1489
        $str = str_replace(["\r", "\n"], '', $str);
1490

1491
        if ($this->charset === 'UTF-8') {
1492
            // Note: We used to have mb_encode_mimeheader() as the first choice
1493
            // here, but it turned out to be buggy and unreliable. DO NOT
1494
            // re-add it! -- Narf
1495
            if (extension_loaded('iconv')) {
1496
                $output = @iconv_mime_encode('', $str, [
1497
                    'scheme'           => 'Q',
1498
                    'line-length'      => 76,
1499
                    'input-charset'    => $this->charset,
1500
                    'output-charset'   => $this->charset,
1501
                    'line-break-chars' => $this->CRLF,
1502
                ]);
1503

1504
                // There are reports that iconv_mime_encode() might fail and return FALSE
1505
                if ($output !== false) {
1506
                    // iconv_mime_encode() will always put a header field name.
1507
                    // We've passed it an empty one, but it still prepends our
1508
                    // encoded string with ': ', so we need to strip it.
1509
                    return static::substr($output, 2);
1510
                }
1511

1512
                $chars = iconv_strlen($str, 'UTF-8');
1513
            } elseif (extension_loaded('mbstring')) {
1514
                $chars = mb_strlen($str, 'UTF-8');
1515
            }
1516
        }
1517

1518
        // We might already have this set for UTF-8
1519
        if (! isset($chars)) {
1520
            $chars = static::strlen($str);
1521
        }
1522

1523
        $output = '=?' . $this->charset . '?Q?';
1524

1525
        for ($i = 0, $length = static::strlen($output); $i < $chars; $i++) {
1526
            $chr = ($this->charset === 'UTF-8' && extension_loaded('iconv')) ? '=' . implode('=', str_split(strtoupper(bin2hex(iconv_substr($str, $i, 1, $this->charset))), 2)) : '=' . strtoupper(bin2hex($str[$i]));
1527

1528
            // RFC 2045 sets a limit of 76 characters per line.
1529
            // We'll append ?= to the end of each line though.
1530
            if ($length + ($l = static::strlen($chr)) > 74) {
1531
                $output .= '?=' . $this->CRLF // EOL
1532
                    . ' =?' . $this->charset . '?Q?' . $chr; // New line
1533

1534
                $length = 6 + static::strlen($this->charset) + $l; // Reset the length for the new line
1535
            } else {
1536
                $output .= $chr;
1537
                $length += $l;
1538
            }
1539
        }
1540

1541
        // End the header
1542
        return $output . '?=';
1543
    }
1544

1545
    /**
1546
     * @param bool $autoClear
1547
     *
1548
     * @return bool
1549
     */
1550
    public function send($autoClear = true)
1551
    {
1552
        if (! isset($this->headers['From']) && ! empty($this->fromEmail)) {
1553
            $this->setFrom($this->fromEmail, $this->fromName);
1554
        }
1555

1556
        if (! isset($this->headers['From'])) {
1557
            $this->setErrorMessage(lang('Email.noFrom'));
1558

1559
            return false;
1560
        }
1561

1562
        if ($this->replyToFlag === false) {
1563
            $this->setReplyTo($this->headers['From']);
1564
        }
1565

1566
        if (
1567
            empty($this->recipients) && ! isset($this->headers['To'])
1568
            && empty($this->BCCArray) && ! isset($this->headers['Bcc'])
1569
            && ! isset($this->headers['Cc'])
1570
        ) {
1571
            $this->setErrorMessage(lang('Email.noRecipients'));
1572

1573
            return false;
1574
        }
1575

1576
        $this->buildHeaders();
1577

1578
        if ($this->BCCBatchMode && count($this->BCCArray) > $this->BCCBatchSize) {
1579
            $this->batchBCCSend();
1580

1581
            if ($autoClear) {
1582
                $this->clear();
1583
            }
1584

1585
            return true;
1586
        }
1587

1588
        $this->buildMessage();
1589
        $result = $this->spoolEmail();
1590

1591
        if ($result) {
1592
            $this->setArchiveValues();
1593

1594
            if ($autoClear) {
1595
                $this->clear();
1596
            }
1597

1598
            Events::trigger('email', $this->archive);
1599
        }
1600

1601
        return $result;
1602
    }
1603

1604
    /**
1605
     * Batch Bcc Send. Sends groups of BCCs in batches
1606
     *
1607
     * @return void
1608
     */
1609
    public function batchBCCSend()
1610
    {
1611
        $float = $this->BCCBatchSize - 1;
1612
        $set   = '';
1613
        $chunk = [];
1614

1615
        for ($i = 0, $c = count($this->BCCArray); $i < $c; $i++) {
1616
            if (isset($this->BCCArray[$i])) {
1617
                $set .= ', ' . $this->BCCArray[$i];
1618
            }
1619

1620
            if ($i === $float) {
1621
                $chunk[] = static::substr($set, 1);
1622
                $float += $this->BCCBatchSize;
1623
                $set = '';
1624
            }
1625

1626
            if ($i === $c - 1) {
1627
                $chunk[] = static::substr($set, 1);
1628
            }
1629
        }
1630

1631
        for ($i = 0, $c = count($chunk); $i < $c; $i++) {
1632
            unset($this->headers['Bcc']);
1633
            $bcc = $this->cleanEmail($this->stringToArray($chunk[$i]));
1634

1635
            if ($this->protocol !== 'smtp') {
1636
                $this->setHeader('Bcc', implode(', ', $bcc));
1637
            } else {
1638
                $this->BCCArray = $bcc;
1639
            }
1640

1641
            $this->buildMessage();
1642
            $this->spoolEmail();
1643
        }
1644

1645
        // Update the archive
1646
        $this->setArchiveValues();
1647
        Events::trigger('email', $this->archive);
1648
    }
1649

1650
    /**
1651
     * Unwrap special elements
1652
     *
1653
     * @return void
1654
     */
1655
    protected function unwrapSpecials()
1656
    {
1657
        $this->finalBody = preg_replace_callback(
1658
            '/\{unwrap\}(.*?)\{\/unwrap\}/si',
1659
            $this->removeNLCallback(...),
1660
            $this->finalBody
1661
        );
1662
    }
1663

1664
    /**
1665
     * Strip line-breaks via callback
1666
     *
1667
     * @used-by unwrapSpecials()
1668
     *
1669
     * @param list<string> $matches
1670
     *
1671
     * @return string
1672
     */
1673
    protected function removeNLCallback($matches)
1674
    {
1675
        if (str_contains($matches[1], "\r") || str_contains($matches[1], "\n")) {
1676
            $matches[1] = str_replace(["\r\n", "\r", "\n"], '', $matches[1]);
1677
        }
1678

1679
        return $matches[1];
1680
    }
1681

1682
    /**
1683
     * Spool mail to the mail server
1684
     *
1685
     * @return bool
1686
     */
1687
    protected function spoolEmail()
1688
    {
1689
        $this->unwrapSpecials();
1690
        $protocol = $this->getProtocol();
1691
        $method   = 'sendWith' . ucfirst($protocol);
1692

1693
        try {
1694
            $success = $this->{$method}();
1695
        } catch (ErrorException $e) {
1696
            $success = false;
1697
            log_message('error', 'Email: ' . $method . ' throwed ' . $e);
1698
        }
1699

1700
        if (! $success) {
1701
            $message = lang('Email.sendFailure' . ($protocol === 'mail' ? 'PHPMail' : ucfirst($protocol)));
1702

1703
            log_message('error', 'Email: ' . $message);
1704
            log_message('error', $this->printDebuggerRaw());
1705

1706
            $this->setErrorMessage($message);
1707

1708
            return false;
1709
        }
1710

1711
        $this->setErrorMessage(lang('Email.sent', [$protocol]));
1712

1713
        return true;
1714
    }
1715

1716
    /**
1717
     * Validate email for shell
1718
     *
1719
     * Applies stricter, shell-safe validation to email addresses.
1720
     * Introduced to prevent RCE via sendmail's -f option.
1721
     *
1722
     * @see     https://github.com/codeigniter4/CodeIgniter/issues/4963
1723
     * @see     https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36
1724
     *
1725
     * @license https://creativecommons.org/publicdomain/zero/1.0/    CC0 1.0, Public Domain
1726
     *
1727
     * Credits for the base concept go to Paul Buonopane <paul@namepros.com>
1728
     *
1729
     * @param string $email
1730
     *
1731
     * @return bool
1732
     */
1733
    protected function validateEmailForShell(&$email)
1734
    {
1735
        if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@')) {
1736
            $email = static::substr($email, 0, ++$atpos)
1737
                . idn_to_ascii(static::substr($email, $atpos), 0, INTL_IDNA_VARIANT_UTS46);
1738
        }
1739

1740
        return filter_var($email, FILTER_VALIDATE_EMAIL) === $email && preg_match('#\A[a-z0-9._+-]+@[a-z0-9.-]{1,253}\z#i', $email);
1741
    }
1742

1743
    /**
1744
     * Send using mail()
1745
     *
1746
     * @return bool
1747
     */
1748
    protected function sendWithMail()
1749
    {
1750
        $recipients = is_array($this->recipients) ? implode(', ', $this->recipients) : $this->recipients;
1751

1752
        // _validate_email_for_shell() below accepts by reference,
1753
        // so this needs to be assigned to a variable
1754
        $from = $this->cleanEmail($this->headers['Return-Path']);
1755

1756
        if (! $this->validateEmailForShell($from)) {
1757
            return mail($recipients, $this->subject, $this->finalBody, $this->headerStr);
1758
        }
1759

1760
        // most documentation of sendmail using the "-f" flag lacks a space after it, however
1761
        // we've encountered servers that seem to require it to be in place.
1762
        return mail($recipients, $this->subject, $this->finalBody, $this->headerStr, '-f ' . $from);
1763
    }
1764

1765
    /**
1766
     * Send using Sendmail
1767
     *
1768
     * @return bool
1769
     */
1770
    protected function sendWithSendmail()
1771
    {
1772
        // _validate_email_for_shell() below accepts by reference,
1773
        // so this needs to be assigned to a variable
1774
        $from = $this->cleanEmail($this->headers['From']);
1775

1776
        $from = $this->validateEmailForShell($from) ? '-f ' . $from : '';
1777

1778
        if (! function_usable('popen') || false === ($fp = @popen($this->mailPath . ' -oi ' . $from . ' -t', 'w'))) {
1779
            return false;
1780
        }
1781

1782
        fwrite($fp, $this->headerStr);
1783
        fwrite($fp, $this->finalBody);
1784
        $status = pclose($fp);
1785

1786
        if ($status !== 0) {
1787
            $this->setErrorMessage(lang('Email.exitStatus', [$status]));
1788
            $this->setErrorMessage(lang('Email.noSocket'));
1789

1790
            return false;
1791
        }
1792

1793
        return true;
1794
    }
1795

1796
    /**
1797
     * Send using SMTP
1798
     *
1799
     * @return bool
1800
     */
1801
    protected function sendWithSmtp()
1802
    {
1803
        if ($this->SMTPHost === '') {
1804
            $this->setErrorMessage(lang('Email.noHostname'));
1805

1806
            return false;
1807
        }
1808

1809
        if (! $this->SMTPConnect() || ! $this->SMTPAuthenticate()) {
1810
            return false;
1811
        }
1812

1813
        if (! $this->sendCommand('from', $this->cleanEmail($this->headers['From']))) {
1814
            $this->SMTPEnd();
1815

1816
            return false;
1817
        }
1818

1819
        foreach ($this->recipients as $val) {
1820
            if (! $this->sendCommand('to', $val)) {
1821
                $this->SMTPEnd();
1822

1823
                return false;
1824
            }
1825
        }
1826

1827
        foreach ($this->CCArray as $val) {
1828
            if ($val !== '' && ! $this->sendCommand('to', $val)) {
1829
                $this->SMTPEnd();
1830

1831
                return false;
1832
            }
1833
        }
1834

1835
        foreach ($this->BCCArray as $val) {
1836
            if ($val !== '' && ! $this->sendCommand('to', $val)) {
1837
                $this->SMTPEnd();
1838

1839
                return false;
1840
            }
1841
        }
1842

1843
        if (! $this->sendCommand('data')) {
1844
            $this->SMTPEnd();
1845

1846
            return false;
1847
        }
1848

1849
        // perform dot transformation on any lines that begin with a dot
1850
        $this->sendData($this->headerStr . preg_replace('/^\./m', '..$1', $this->finalBody));
1851
        $this->sendData($this->newline . '.');
1852
        $reply = $this->getSMTPData();
1853
        $this->setErrorMessage($reply);
1854
        $this->SMTPEnd();
1855

1856
        if (! str_starts_with($reply, '250')) {
1857
            $this->setErrorMessage(lang('Email.SMTPError', [$reply]));
1858

1859
            return false;
1860
        }
1861

1862
        return true;
1863
    }
1864

1865
    /**
1866
     * Shortcut to send RSET or QUIT depending on keep-alive
1867
     *
1868
     * @return void
1869
     */
1870
    protected function SMTPEnd()
1871
    {
1872
        $this->sendCommand($this->SMTPKeepAlive ? 'reset' : 'quit');
1873
    }
1874

1875
    /**
1876
     * @return bool|string
1877
     */
1878
    protected function SMTPConnect()
1879
    {
1880
        if (is_resource($this->SMTPConnect)) {
1881
            return true;
1882
        }
1883

1884
        $ssl = '';
1885

1886
        // Connection to port 465 should use implicit TLS (without STARTTLS)
1887
        // as per RFC 8314.
1888
        if ($this->SMTPPort === 465) {
1889
            $ssl = 'tls://';
1890
        }
1891
        // But if $SMTPCrypto is set to `ssl`, SSL can be used.
1892
        if ($this->SMTPCrypto === 'ssl') {
1893
            $ssl = 'ssl://';
1894
        }
1895

1896
        $this->SMTPConnect = fsockopen(
1897
            $ssl . $this->SMTPHost,
1898
            $this->SMTPPort,
1899
            $errno,
1900
            $errstr,
1901
            $this->SMTPTimeout
1902
        );
1903

1904
        if (! is_resource($this->SMTPConnect)) {
1905
            $this->setErrorMessage(lang('Email.SMTPError', [$errno . ' ' . $errstr]));
1906

1907
            return false;
1908
        }
1909

1910
        stream_set_timeout($this->SMTPConnect, $this->SMTPTimeout);
1911
        $this->setErrorMessage($this->getSMTPData());
1912

1913
        if ($this->SMTPCrypto === 'tls') {
1914
            $this->sendCommand('hello');
1915
            $this->sendCommand('starttls');
1916
            $crypto = stream_socket_enable_crypto(
1917
                $this->SMTPConnect,
1918
                true,
1919
                STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT
1920
                | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
1921
                | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
1922
                | STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT
1923
            );
1924

1925
            if ($crypto !== true) {
1926
                $this->setErrorMessage(lang('Email.SMTPError', [$this->getSMTPData()]));
1927

1928
                return false;
1929
            }
1930
        }
1931

1932
        return $this->sendCommand('hello');
1933
    }
1934

1935
    /**
1936
     * @param string $cmd
1937
     * @param string $data
1938
     *
1939
     * @return bool
1940
     */
1941
    protected function sendCommand($cmd, $data = '')
1942
    {
1943
        switch ($cmd) {
1944
            case 'hello':
1945
                if ($this->SMTPAuth || $this->getEncoding() === '8bit') {
1946
                    $this->sendData('EHLO ' . $this->getHostname());
1947
                } else {
1948
                    $this->sendData('HELO ' . $this->getHostname());
1949
                }
1950

1951
                $resp = 250;
1952
                break;
1953

1954
            case 'starttls':
1955
                $this->sendData('STARTTLS');
1956
                $resp = 220;
1957
                break;
1958

1959
            case 'from':
1960
                $this->sendData('MAIL FROM:<' . $data . '>');
1961
                $resp = 250;
1962
                break;
1963

1964
            case 'to':
1965
                if ($this->DSN) {
1966
                    $this->sendData('RCPT TO:<' . $data . '> NOTIFY=SUCCESS,DELAY,FAILURE ORCPT=rfc822;' . $data);
1967
                } else {
1968
                    $this->sendData('RCPT TO:<' . $data . '>');
1969
                }
1970
                $resp = 250;
1971
                break;
1972

1973
            case 'data':
1974
                $this->sendData('DATA');
1975
                $resp = 354;
1976
                break;
1977

1978
            case 'reset':
1979
                $this->sendData('RSET');
1980
                $resp = 250;
1981
                break;
1982

1983
            case 'quit':
1984
                $this->sendData('QUIT');
1985
                $resp = 221;
1986
                break;
1987

1988
            default:
1989
                $resp = null;
1990
        }
1991

1992
        $reply = $this->getSMTPData();
1993

1994
        $this->debugMessage[]    = '<pre>' . $cmd . ': ' . $reply . '</pre>';
1995
        $this->debugMessageRaw[] = $cmd . ': ' . $reply;
1996

1997
        if ($resp === null || ((int) static::substr($reply, 0, 3) !== $resp)) {
1998
            $this->setErrorMessage(lang('Email.SMTPError', [$reply]));
1999

2000
            return false;
2001
        }
2002

2003
        if ($cmd === 'quit') {
2004
            fclose($this->SMTPConnect);
2005
        }
2006

2007
        return true;
2008
    }
2009

2010
    /**
2011
     * @return bool
2012
     */
2013
    protected function SMTPAuthenticate()
2014
    {
2015
        if (! $this->SMTPAuth) {
2016
            return true;
2017
        }
2018

2019
        if ($this->SMTPUser === '' && $this->SMTPPass === '') {
2020
            $this->setErrorMessage(lang('Email.noSMTPAuth'));
2021

2022
            return false;
2023
        }
2024

2025
        $this->sendData('AUTH LOGIN');
2026
        $reply = $this->getSMTPData();
2027

2028
        if (str_starts_with($reply, '503')) {    // Already authenticated
2029
            return true;
2030
        }
2031

2032
        if (! str_starts_with($reply, '334')) {
2033
            $this->setErrorMessage(lang('Email.failedSMTPLogin', [$reply]));
2034

2035
            return false;
2036
        }
2037

2038
        $this->sendData(base64_encode($this->SMTPUser));
2039
        $reply = $this->getSMTPData();
2040

2041
        if (! str_starts_with($reply, '334')) {
2042
            $this->setErrorMessage(lang('Email.SMTPAuthUsername', [$reply]));
2043

2044
            return false;
2045
        }
2046

2047
        $this->sendData(base64_encode($this->SMTPPass));
2048
        $reply = $this->getSMTPData();
2049

2050
        if (! str_starts_with($reply, '235')) {
2051
            $this->setErrorMessage(lang('Email.SMTPAuthPassword', [$reply]));
2052

2053
            return false;
2054
        }
2055

2056
        if ($this->SMTPKeepAlive) {
2057
            $this->SMTPAuth = false;
2058
        }
2059

2060
        return true;
2061
    }
2062

2063
    /**
2064
     * @param string $data
2065
     *
2066
     * @return bool
2067
     */
2068
    protected function sendData($data)
2069
    {
2070
        $data .= $this->newline;
2071

2072
        $result = null;
2073

2074
        for ($written = $timestamp = 0, $length = static::strlen($data); $written < $length; $written += $result) {
2075
            if (($result = fwrite($this->SMTPConnect, static::substr($data, $written))) === false) {
2076
                break;
2077
            }
2078

2079
            // See https://bugs.php.net/bug.php?id=39598 and http://php.net/manual/en/function.fwrite.php#96951
2080
            if ($result === 0) {
2081
                if ($timestamp === 0) {
2082
                    $timestamp = Time::now()->getTimestamp();
2083
                } elseif ($timestamp < (Time::now()->getTimestamp() - $this->SMTPTimeout)) {
2084
                    $result = false;
2085

2086
                    break;
2087
                }
2088

2089
                usleep(250000);
2090

2091
                continue;
2092
            }
2093

2094
            $timestamp = 0;
2095
        }
2096

2097
        if (! is_int($result)) {
2098
            $this->setErrorMessage(lang('Email.SMTPDataFailure', [$data]));
2099

2100
            return false;
2101
        }
2102

2103
        return true;
2104
    }
2105

2106
    /**
2107
     * @return string
2108
     */
2109
    protected function getSMTPData()
2110
    {
2111
        $data = '';
2112

2113
        while ($str = fgets($this->SMTPConnect, 512)) {
2114
            $data .= $str;
2115

2116
            if ($str[3] === ' ') {
2117
                break;
2118
            }
2119
        }
2120

2121
        return $data;
2122
    }
2123

2124
    /**
2125
     * There are only two legal types of hostname - either a fully
2126
     * qualified domain name (eg: "mail.example.com") or an IP literal
2127
     * (eg: "[1.2.3.4]").
2128
     *
2129
     * @see https://tools.ietf.org/html/rfc5321#section-2.3.5
2130
     * @see http://cbl.abuseat.org/namingproblems.html
2131
     *
2132
     * @return string
2133
     */
2134
    protected function getHostname()
2135
    {
2136
        if (isset($_SERVER['SERVER_NAME'])) {
2137
            return $_SERVER['SERVER_NAME'];
2138
        }
2139

2140
        if (isset($_SERVER['SERVER_ADDR'])) {
2141
            return '[' . $_SERVER['SERVER_ADDR'] . ']';
2142
        }
2143

2144
        $hostname = gethostname();
2145
        if ($hostname !== false) {
2146
            return $hostname;
2147
        }
2148

2149
        return '[127.0.0.1]';
2150
    }
2151

2152
    /**
2153
     * @param array|string $include List of raw data chunks to include in the output
2154
     *                              Valid options are: 'headers', 'subject', 'body'
2155
     *
2156
     * @return string
2157
     */
2158
    public function printDebugger($include = ['headers', 'subject', 'body'])
2159
    {
2160
        $msg = implode('', $this->debugMessage);
2161

2162
        // Determine which parts of our raw data needs to be printed
2163
        $rawData = '';
2164

2165
        if (! is_array($include)) {
2166
            $include = [$include];
2167
        }
2168

2169
        if (in_array('headers', $include, true)) {
2170
            $rawData = htmlspecialchars($this->headerStr) . "\n";
2171
        }
2172
        if (in_array('subject', $include, true)) {
2173
            $rawData .= htmlspecialchars($this->subject) . "\n";
2174
        }
2175
        if (in_array('body', $include, true)) {
2176
            $rawData .= htmlspecialchars($this->finalBody);
2177
        }
2178

2179
        return $msg . ($rawData === '' ? '' : '<pre>' . $rawData . '</pre>');
2180
    }
2181

2182
    /**
2183
     * Returns raw debug messages
2184
     */
2185
    private function printDebuggerRaw(): string
2186
    {
2187
        return implode("\n", $this->debugMessageRaw);
2188
    }
2189

2190
    /**
2191
     * @param string $msg
2192
     *
2193
     * @return void
2194
     */
2195
    protected function setErrorMessage($msg)
2196
    {
2197
        $this->debugMessage[]    = $msg . '<br>';
2198
        $this->debugMessageRaw[] = $msg;
2199
    }
2200

2201
    /**
2202
     * Mime Types
2203
     *
2204
     * @param string $ext
2205
     *
2206
     * @return string
2207
     */
2208
    protected function mimeTypes($ext = '')
2209
    {
2210
        $mime = Mimes::guessTypeFromExtension(strtolower($ext));
2211

2212
        return ! empty($mime) ? $mime : 'application/x-unknown-content-type';
2213
    }
2214

2215
    public function __destruct()
2216
    {
2217
        if (is_resource($this->SMTPConnect)) {
2218
            try {
2219
                $this->sendCommand('quit');
2220
            } catch (ErrorException $e) {
2221
                $protocol = $this->getProtocol();
2222
                $method   = 'sendWith' . ucfirst($protocol);
2223
                log_message('error', 'Email: ' . $method . ' throwed ' . $e);
2224
            }
2225
        }
2226
    }
2227

2228
    /**
2229
     * Byte-safe strlen()
2230
     *
2231
     * @param string $str
2232
     *
2233
     * @return int
2234
     */
2235
    protected static function strlen($str)
2236
    {
2237
        return (static::$func_overload) ? mb_strlen($str, '8bit') : strlen($str);
2238
    }
2239

2240
    /**
2241
     * Byte-safe substr()
2242
     *
2243
     * @param string   $str
2244
     * @param int      $start
2245
     * @param int|null $length
2246
     *
2247
     * @return string
2248
     */
2249
    protected static function substr($str, $start, $length = null)
2250
    {
2251
        if (static::$func_overload) {
2252
            return mb_substr($str, $start, $length, '8bit');
2253
        }
2254

2255
        return isset($length) ? substr($str, $start, $length) : substr($str, $start);
2256
    }
2257

2258
    /**
2259
     * Determines the values that should be stored in $archive.
2260
     *
2261
     * @return array The updated archive values
2262
     */
2263
    protected function setArchiveValues(): array
2264
    {
2265
        // Get property values and add anything prepped in tmpArchive
2266
        $this->archive = array_merge(get_object_vars($this), $this->tmpArchive);
2267
        unset($this->archive['archive']);
2268

2269
        // Clear tmpArchive for next run
2270
        $this->tmpArchive = [];
2271

2272
        return $this->archive;
2273
    }
2274
}
2275

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.