Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 142 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
MailHandler | |
0.00% |
0 / 142 |
|
0.00% |
0 / 11 |
5700 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
__destruct | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setMailer | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
72 | |||
send | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
postSend | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
42 | |||
sendmailSend | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
mailSend | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
72 | |||
mailPassthru | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
smtpSend | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
110 | |||
smtpConnect | |
0.00% |
0 / 56 |
|
0.00% |
0 / 1 |
756 | |||
smtpClose | |
0.00% |
0 / 3 |
|
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 | */ |
17 | declare(strict_types=1); |
18 | |
19 | namespace phpOMS\Message\Mail; |
20 | |
21 | use phpOMS\System\SystemUtils; |
22 | use phpOMS\Utils\StringUtils; |
23 | use phpOMS\Validation\Network\Email as EmailValidator; |
24 | use 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 | */ |
34 | class 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 | } |