Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
MailHandler
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 11
5700
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setMailer
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
72
 send
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 postSend
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
42
 sendmailSend
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 mailSend
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
72
 mailPassthru
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 smtpSend
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
110
 smtpConnect
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
756
 smtpClose
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Message\Mail
8 * @license   GLGPL 2.1 License
9 * @version   1.0.0
10 * @link      https://jingga.app
11 *
12 * Extended based on:
13 * GLGPL 2.1 License
14 * (c) 2012 - 2015 Marcus Bointon, 2010 - 2012 Jim Jagielski, 2004 - 2009 Andy Prevost
15 * (c) PHPMailer
16 */
17declare(strict_types=1);
18
19namespace phpOMS\Message\Mail;
20
21use phpOMS\System\SystemUtils;
22use phpOMS\Utils\StringUtils;
23use phpOMS\Validation\Network\Email as EmailValidator;
24use phpOMS\Validation\Network\Hostname;
25
26/**
27 * Mail class.
28 *
29 * @package phpOMS\Message\Mail
30 * @license GLGPL 2.1 License
31 * @link    https://jingga.app
32 * @since   1.0.0
33 */
34class MailHandler
35{
36    /**
37     * The maximum line length allowed by RFC 2822 section 2.1.1.
38     *
39     * @var int
40     * @since 1.0.0
41     */
42    public const MAX_LINE_LENGTH = 998;
43
44    /**
45     * Mailer for sending message
46     *
47     * @var string
48     * @since 1.0.0
49     */
50    public string $mailer = SubmitType::MAIL;
51
52    /**
53     * The path to the sendmail program.
54     *
55     * @var string
56     * @since 1.0.0
57     */
58    public string $mailerTool = '';
59
60    /**
61     * Use sendmail MTA
62     *
63     * @var bool
64     * @since 1.0.0
65     */
66    public bool $useMailOptions = true;
67
68    /**
69     * Hostname for Message-ID and HELO string.
70     *
71     * If empty this is automatically generated.
72     *
73     * @var string
74     * @since 1.0.0
75     */
76    public string $hostname = '';
77
78    /**
79     * SMTP hosts.
80     * (e.g. "smtp1.example.com:25;smtp2.example.com").
81     *
82     * @var string
83     * @since 1.0.0
84     */
85    public string $host = 'localhost';
86
87    /**
88     * The default port.
89     *
90     * @var int
91     * @since 1.0.0
92     */
93    public int $port = 25;
94
95    /**
96     * The SMTP HELO/EHLO name
97     *
98     * @var string
99     * @since 1.0.0
100     */
101    public string $helo = '';
102
103    /**
104     * SMTP encryption
105     *
106     * @var string
107     * @since 1.0.0
108     */
109    public string $encryption = EncryptionType::NONE;
110
111    /**
112     * Use TLS automatically if the server supports it.
113     *
114     * @var bool
115     * @since 1.0.0
116     */
117    public bool $useAutoTLS = true;
118
119    /**
120     * Options passed when connecting via SMTP.
121     *
122     * @var array
123     * @since 1.0.0
124     */
125    public array $smtpOptions = [];
126
127    /**
128     * SMTP username.
129     *
130     * @var string
131     * @since 1.0.0
132     */
133    public string $username = '';
134
135    /**
136     * SMTP password.
137     *
138     * @var string
139     * @since 1.0.0
140     */
141    public string $password = '';
142
143    /**
144     * SMTP auth type.
145     *
146     * @var string
147     * @since 1.0.0
148     */
149    public string $authType = SMTPAuthType::NONE;
150
151    /**
152     * OAuth class.
153     *
154     * @var OAuth
155     * @since 1.0.0
156     */
157    public mixed $oauth = null;
158
159    /**
160     * Server timeout
161     *
162     * @var int
163     * @since 1.0.0
164     */
165    public int $timeout = 300;
166
167    /**
168     * Comma separated list of DSN notifications
169     *
170     * @var string
171     * @since 1.0.0
172     */
173    public string $dsn = DsnNotificationType::NONE;
174
175    /**
176     * Keep connection alive.
177     *
178     * This requires a close call.
179     *
180     * @var bool
181     * @since 1.0.0
182     */
183    public bool $keepAlive = false;
184
185    /**
186     * Use VERP
187     *
188     * @var bool
189     * @since 1.0.0
190     */
191    public bool $useVerp = false;
192
193    /**
194     * An instance of the SMTP sender class.
195     *
196     * @var null|Smtp
197     * @since 1.0.0
198     */
199    public ?Smtp $smtp = null;
200
201    /**
202     * SMTP RFC standard line ending
203     *
204     * @var string
205     * @since 1.0.0
206     */
207    protected static string $LE = "\r\n";
208
209    /**
210     * Constructor.
211     *
212     * @param string $user       Username
213     * @param string $pass       Password
214     * @param int    $port       Port
215     * @param string $encryption Encryption type
216     *
217     * @since 1.0.0
218     */
219    public function __construct(string $user = '', string $pass = '', int $port = 25, string $encryption = EncryptionType::NONE)
220    {
221        $this->username   = $user;
222        $this->password   = $pass;
223        $this->port       = $port;
224        $this->encryption = $encryption;
225    }
226
227    /**
228     * Destructor.
229     *
230     * @since 1.0.0
231     */
232    public function __destruct()
233    {
234        $this->smtpClose();
235    }
236
237    /**
238     * Set the mailer and the mailer tool
239     *
240     * @param string $mailer Mailer
241     *
242     * @return void
243     *
244     * @since 1.0.0
245     */
246    public function setMailer(string $mailer) : void
247    {
248        $this->mailer = $mailer;
249
250        switch ($mailer) {
251            case SubmitType::MAIL:
252            case SubmitType::SMTP:
253                return;
254            case SubmitType::SENDMAIL:
255                $this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'sendmail') === false
256                    ? '/usr/sbin/sendmail'
257                    : $sendmailPath;
258                return;
259            case SubmitType::QMAIL:
260                $this->mailerTool = \stripos($sendmailPath = \ini_get('sendmail_path'), 'qmail') === false
261                    ? '/var/qmail/bin/qmail-inject'
262                    : $sendmailPath;
263                return;
264            default:
265                return;
266        }
267    }
268
269    /**
270     * Send mail
271     *
272     * @param $mail Mail
273     *
274     * @return bool
275     *
276     * @since 1.0.0
277     */
278    public function send(Email $mail) : bool
279    {
280        if (!$mail->preSend($this->mailer)) {
281            return false;
282        }
283
284        return $this->postSend($mail);
285    }
286
287    /**
288     * Send the mail
289     *
290     * @param Email $mail Mail
291     *
292     * @return bool
293     *
294     * @since 1.0.0
295     */
296    private function postSend(Email $mail) : bool
297    {
298        switch ($this->mailer) {
299            case SubmitType::SENDMAIL:
300            case SubmitType::QMAIL:
301                return $this->sendmailSend($mail);
302            case SubmitType::SMTP:
303                return $this->smtpSend($mail);
304            case SubmitType::MAIL:
305                return $this->mailSend($mail);
306            default:
307                return false;
308        }
309    }
310
311    /**
312     * Send mail
313     *
314     * @param Email $mail Mail
315     *
316     * @return bool
317     *
318     * @since 1.0.0
319     */
320    protected function sendmailSend(Email $mail) : bool
321    {
322        $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
323
324        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
325        if (!empty($mail->sender) && StringUtils::isShellSafe($mail->sender)) {
326            $mailerToolFmt = $this->mailer === SubmitType::QMAIL
327                ? '%s -f%s'
328                : '%s -oi -f%s -t';
329        } elseif ($this->mailer === SubmitType::QMAIL) {
330            $mailerToolFmt = '%s';
331        } else {
332            $mailerToolFmt = '%s -oi -t';
333        }
334
335        $mailerTool = \sprintf($mailerToolFmt, \escapeshellcmd($this->mailerTool), $mail->sender);
336
337        $con = \popen($mailerTool, 'w');
338        if ($con === false) {
339            return false;
340        }
341
342        \fwrite($con, $header);
343        \fwrite($con, $mail->bodyMime);
344
345        $result = \pclose($con);
346
347        return $result === 0;
348    }
349
350    /**
351     * Send mail
352     *
353     * @param Email $mail Mail
354     *
355     * @return bool
356     *
357     * @since 1.0.0
358     */
359    protected function mailSend(Email $mail) : bool
360    {
361        $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
362
363        $toArr = [];
364        foreach ($mail->to as $toaddr) {
365            $toArr[] = $mail->addrFormat($toaddr);
366        }
367
368        $to = \implode(', ', $toArr);
369
370        //This sets the SMTP envelope sender which gets turned into a return-path header by the receiver
371        // CVE-2016-10033, CVE-2016-10045: Don't pass -f if characters will be escaped.
372        $params = null;
373        if (!empty($mail->sender)
374            && EmailValidator::isValid($mail->sender)
375            && StringUtils::isShellSafe($mail->sender)
376        ) {
377            $params = \sprintf('-f%s', $mail->sender);
378        }
379
380        $oldFrom = '';
381        if (!empty($mail->sender) && EmailValidator::isValid($mail->sender)) {
382            $oldFrom = \ini_get('sendmail_from');
383            \ini_set('sendmail_from', $mail->sender);
384        }
385
386        $result = $this->mailPassthru($to, $mail, $header, $params);
387
388        if (!empty($oldFrom)) {
389            \ini_set('sendmail_from', $oldFrom);
390        }
391
392        return $result;
393    }
394
395    /**
396     * Call mail() in a safe_mode-aware fashion.
397     *
398     * @param string      $to     To
399     * @param Email       $mail   Mail
400     * @param string      $body   Message Body
401     * @param string      $header Additional Header(s)
402     * @param null|string $params Params
403     *
404     * @return bool
405     *
406     * @since 1.0.0
407     */
408    private function mailPassthru(string $to, Email $mail, string $header, string $params = null) : bool
409    {
410        $subject = $mail->encodeHeader(\trim(\str_replace(["\r", "\n"], '', $mail->subject)));
411
412        return !$this->useMailOptions || $params === null
413            ? \mail($to, $subject, $mail->bodyMime, $header)
414            : \mail($to, $subject, $mail->bodyMime, $header, $params);
415    }
416
417    /**
418     * Send mail
419     *
420     * @param Email $mail Mail
421     *
422     * @return bool
423     *
424     * @since 1.0.0
425     */
426    protected function smtpSend(Email $mail) : bool
427    {
428        $header = \rtrim($mail->headerMime, " \r\n\t") . self::$LE . self::$LE;
429
430        if (!$this->smtpConnect($this->smtpOptions)) {
431            return false;
432        }
433
434        $mail->hostname = $this->hostname;
435
436        $smtpFrom = $mail->sender === '' ? $mail->from[0] : $mail->sender;
437
438        if (!$this->smtp->mail($smtpFrom)) {
439            return false;
440        }
441
442        $badRcpt   = [];
443        $receivers = [$mail->to, $mail->cc, $mail->bcc];
444        foreach ($receivers as $togroup) {
445            foreach ($togroup as $to) {
446                if (!$this->smtp->recipient($to[0], $this->dsn)) {
447                    $badRcpt[] = $to[0];
448                }
449            }
450        }
451
452        // Only send the DATA command if we have viable recipients
453        if ((\count($mail->to) + \count($mail->cc) + \count($mail->bcc) > \count($badRcpt))
454            && !$this->smtp->data($header . $mail->bodyMime, self::MAX_LINE_LENGTH)
455        ) {
456            return false;
457        }
458
459        //$transactinoId = $this->smtp->getLastTransactionId();
460
461        if ($this->keepAlive) {
462            $this->smtp->reset();
463        } else {
464            $this->smtp->quit();
465            $this->smtp->close();
466        }
467
468        return empty($badRcpt);
469    }
470
471    /**
472     * Initiate a connection to an SMTP server.
473     *
474     * @param array $options An array of options compatible with stream_context_create()
475     *
476     * @return bool
477     *
478     * @since 1.0.0
479     */
480    public function smtpConnect(array $options = null) : bool
481    {
482        if ($this->smtp === null) {
483            $this->smtp = new Smtp();
484        }
485
486        if ($this->smtp->isConnected()) {
487            return true;
488        }
489
490        if ($options === null) {
491            $options = $this->smtpOptions;
492        }
493
494        $this->smtp->timeout = $this->timeout;
495        $this->smtp->doVerp  = $this->useVerp;
496
497        $hosts = \explode(';', $this->host);
498        foreach ($hosts as $hostentry) {
499            $hostinfo = [];
500            if (!\preg_match(
501                    '/^(?:(ssl|tls):\/\/)?(.+?)(?::(\d+))?$/',
502                    \trim($hostentry),
503                    $hostinfo
504                )
505            ) {
506                // Not a valid host entry
507                continue;
508            }
509
510            // $hostinfo[1]: optional ssl or tls prefix
511            // $hostinfo[2]: the hostname
512            // $hostinfo[3]: optional port number
513
514            //Check the host name is a valid name or IP address
515            if (!Hostname::isValid($hostinfo[2])) {
516                continue;
517            }
518
519            $prefix = '';
520            $secure = $this->encryption;
521            $tls    = ($this->encryption === EncryptionType::TLS);
522
523            if ($hostinfo[1] === 'ssl' || ($hostinfo[1] === '' && $this->encryption === EncryptionType::SMTPS)) {
524                $prefix = 'ssl://';
525                $tls    = false;
526                $secure = EncryptionType::SMTPS;
527            } elseif ($hostinfo[1] === 'tls') {
528                $tls    = true;
529                $secure = EncryptionType::TLS;
530            }
531
532            //Do we need the OpenSSL extension?
533            $sslExt = \defined('OPENSSL_ALGO_SHA256');
534            if (($secure === EncryptionType::TLS || $secure === EncryptionType::SMTPS)
535                && !$sslExt
536            ) {
537                return false;
538            }
539
540            $host = $hostinfo[2];
541            $port = $this->port;
542
543            if (isset($hostinfo[3])
544                && \is_numeric($hostinfo[3])
545                && $hostinfo[3] > 0 && $hostinfo[3] < 65536
546            ) {
547                $port = (int) $hostinfo[3];
548            }
549
550            if ($this->smtp->connect($prefix . $host, $port, $this->timeout, $options)) {
551                $hello = empty($this->helo) ? SystemUtils::getHostname() : $this->helo;
552
553                $this->smtp->hello($hello);
554
555                //Automatically enable TLS encryption
556                $tls = $this->useAutoTLS
557                    && $sslExt
558                    && $secure !== EncryptionType::SMTPS
559                    && $this->smtp->getServerExt('STARTTLS') === '1'
560                        ? true : $tls;
561
562                if ($tls) {
563                    if (!$this->smtp->startTLS()) {
564                        return false;
565                    }
566
567                    // Resend EHLO
568                    $this->smtp->hello($hello);
569                }
570
571                return $this->smtp === null
572                    ? false
573                    : $this->smtp->authenticate($this->username, $this->password, $this->authType, $this->oauth);
574            }
575        }
576
577        // If we get here, all connection attempts have failed
578        $this->smtp->close();
579
580        return false;
581    }
582
583    /**
584     * Close SMTP
585     *
586     * @return void
587     *
588     * @since 1.0.0
589     */
590    public function smtpClose() : void
591    {
592        if ($this->smtp !== null && $this->smtp->isConnected()) {
593            $this->smtp->quit();
594            $this->smtp->close();
595        }
596    }
597}