11 * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> |
11 * @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net> |
12 * @author Brent R. Matzelle (original founder) |
12 * @author Brent R. Matzelle (original founder) |
13 * @copyright 2012 - 2020 Marcus Bointon |
13 * @copyright 2012 - 2020 Marcus Bointon |
14 * @copyright 2010 - 2012 Jim Jagielski |
14 * @copyright 2010 - 2012 Jim Jagielski |
15 * @copyright 2004 - 2009 Andy Prevost |
15 * @copyright 2004 - 2009 Andy Prevost |
16 * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License |
16 * @license https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html GNU Lesser General Public License |
17 * @note This program is distributed in the hope that it will be useful - WITHOUT |
17 * @note This program is distributed in the hope that it will be useful - WITHOUT |
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
18 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
19 * FITNESS FOR A PARTICULAR PURPOSE. |
19 * FITNESS FOR A PARTICULAR PURPOSE. |
20 */ |
20 */ |
21 |
21 |
60 |
60 |
61 /** |
61 /** |
62 * The maximum line length allowed by RFC 5321 section 4.5.3.1.6, |
62 * The maximum line length allowed by RFC 5321 section 4.5.3.1.6, |
63 * *excluding* a trailing CRLF break. |
63 * *excluding* a trailing CRLF break. |
64 * |
64 * |
65 * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.6 |
65 * @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.6 |
66 * |
66 * |
67 * @var int |
67 * @var int |
68 */ |
68 */ |
69 const MAX_LINE_LENGTH = 998; |
69 const MAX_LINE_LENGTH = 998; |
70 |
70 |
71 /** |
71 /** |
72 * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5, |
72 * The maximum line length allowed for replies in RFC 5321 section 4.5.3.1.5, |
73 * *including* a trailing CRLF line break. |
73 * *including* a trailing CRLF line break. |
74 * |
74 * |
75 * @see https://tools.ietf.org/html/rfc5321#section-4.5.3.1.5 |
75 * @see https://www.rfc-editor.org/rfc/rfc5321#section-4.5.3.1.5 |
76 * |
76 * |
77 * @var int |
77 * @var int |
78 */ |
78 */ |
79 const MAX_REPLY_LENGTH = 512; |
79 const MAX_REPLY_LENGTH = 512; |
80 |
80 |
150 public $Debugoutput = 'echo'; |
150 public $Debugoutput = 'echo'; |
151 |
151 |
152 /** |
152 /** |
153 * Whether to use VERP. |
153 * Whether to use VERP. |
154 * |
154 * |
155 * @see http://en.wikipedia.org/wiki/Variable_envelope_return_path |
155 * @see https://en.wikipedia.org/wiki/Variable_envelope_return_path |
156 * @see http://www.postfix.org/VERP_README.html Info on VERP |
156 * @see https://www.postfix.org/VERP_README.html Info on VERP |
157 * |
157 * |
158 * @var bool |
158 * @var bool |
159 */ |
159 */ |
160 public $do_verp = false; |
160 public $do_verp = false; |
161 |
161 |
162 /** |
162 /** |
163 * The timeout value for connection, in seconds. |
163 * The timeout value for connection, in seconds. |
164 * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. |
164 * Default of 5 minutes (300sec) is from RFC2821 section 4.5.3.2. |
165 * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. |
165 * This needs to be quite high to function correctly with hosts using greetdelay as an anti-spam measure. |
166 * |
166 * |
167 * @see http://tools.ietf.org/html/rfc2821#section-4.5.3.2 |
167 * @see https://www.rfc-editor.org/rfc/rfc2821#section-4.5.3.2 |
168 * |
168 * |
169 * @var int |
169 * @var int |
170 */ |
170 */ |
171 public $Timeout = 300; |
171 public $Timeout = 300; |
172 |
172 |
185 * |
185 * |
186 * @var string[] |
186 * @var string[] |
187 */ |
187 */ |
188 protected $smtp_transaction_id_patterns = [ |
188 protected $smtp_transaction_id_patterns = [ |
189 'exim' => '/[\d]{3} OK id=(.*)/', |
189 'exim' => '/[\d]{3} OK id=(.*)/', |
190 'sendmail' => '/[\d]{3} 2.0.0 (.*) Message/', |
190 'sendmail' => '/[\d]{3} 2\.0\.0 (.*) Message/', |
191 'postfix' => '/[\d]{3} 2.0.0 Ok: queued as (.*)/', |
191 'postfix' => '/[\d]{3} 2\.0\.0 Ok: queued as (.*)/', |
192 'Microsoft_ESMTP' => '/[0-9]{3} 2.[\d].0 (.*)@(?:.*) Queued mail for delivery/', |
192 'Microsoft_ESMTP' => '/[0-9]{3} 2\.[\d]\.0 (.*)@(?:.*) Queued mail for delivery/', |
193 'Amazon_SES' => '/[\d]{3} Ok (.*)/', |
193 'Amazon_SES' => '/[\d]{3} Ok (.*)/', |
194 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', |
194 'SendGrid' => '/[\d]{3} Ok: queued as (.*)/', |
195 'CampaignMonitor' => '/[\d]{3} 2.0.0 OK:([a-zA-Z\d]{48})/', |
195 'CampaignMonitor' => '/[\d]{3} 2\.0\.0 OK:([a-zA-Z\d]{48})/', |
196 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', |
196 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', |
197 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', |
197 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', |
198 'Mailjet' => '/[\d]{3} OK queued as (.*)/', |
198 'Mailjet' => '/[\d]{3} OK queued as (.*)/', |
199 ]; |
199 ]; |
200 |
200 |
278 if ($level > $this->do_debug) { |
278 if ($level > $this->do_debug) { |
279 return; |
279 return; |
280 } |
280 } |
281 //Is this a PSR-3 logger? |
281 //Is this a PSR-3 logger? |
282 if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { |
282 if ($this->Debugoutput instanceof \Psr\Log\LoggerInterface) { |
283 $this->Debugoutput->debug($str); |
283 //Remove trailing line breaks potentially added by calls to SMTP::client_send() |
|
284 $this->Debugoutput->debug(rtrim($str, "\r\n")); |
284 |
285 |
285 return; |
286 return; |
286 } |
287 } |
287 //Avoid clash with built-in function names |
288 //Avoid clash with built-in function names |
288 if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { |
289 if (is_callable($this->Debugoutput) && !in_array($this->Debugoutput, ['error_log', 'html', 'echo'])) { |
291 return; |
292 return; |
292 } |
293 } |
293 switch ($this->Debugoutput) { |
294 switch ($this->Debugoutput) { |
294 case 'error_log': |
295 case 'error_log': |
295 //Don't output, just log |
296 //Don't output, just log |
|
297 /** @noinspection ForgottenDebugOutputInspection */ |
296 error_log($str); |
298 error_log($str); |
297 break; |
299 break; |
298 case 'html': |
300 case 'html': |
299 //Cleans up output a bit for a better looking, HTML-safe output |
301 //Cleans up output a bit for a better looking, HTML-safe output |
300 echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( |
302 echo gmdate('Y-m-d H:i:s'), ' ', htmlentities( |
369 if ($responseCode === 220) { |
371 if ($responseCode === 220) { |
370 return true; |
372 return true; |
371 } |
373 } |
372 //Anything other than a 220 response means something went wrong |
374 //Anything other than a 220 response means something went wrong |
373 //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error |
375 //RFC 5321 says the server will wait for us to send a QUIT in response to a 554 error |
374 //https://tools.ietf.org/html/rfc5321#section-3.1 |
376 //https://www.rfc-editor.org/rfc/rfc5321#section-3.1 |
375 if ($responseCode === 554) { |
377 if ($responseCode === 554) { |
376 $this->quit(); |
378 $this->quit(); |
377 } |
379 } |
378 //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down) |
380 //This will handle 421 responses which may not wait for a QUIT (e.g. if the server is being shut down) |
379 $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION); |
381 $this->edebug('Connection: closing due to error', self::DEBUG_CONNECTION); |
417 //Fall back to fsockopen which should work in more places, but is missing some features |
421 //Fall back to fsockopen which should work in more places, but is missing some features |
418 $this->edebug( |
422 $this->edebug( |
419 'Connection: stream_socket_client not available, falling back to fsockopen', |
423 'Connection: stream_socket_client not available, falling back to fsockopen', |
420 self::DEBUG_CONNECTION |
424 self::DEBUG_CONNECTION |
421 ); |
425 ); |
422 set_error_handler([$this, 'errorHandler']); |
426 set_error_handler(function () { |
|
427 call_user_func_array([$this, 'errorHandler'], func_get_args()); |
|
428 }); |
423 $connection = fsockopen( |
429 $connection = fsockopen( |
424 $host, |
430 $host, |
425 $port, |
431 $port, |
426 $errno, |
432 $errno, |
427 $errstr, |
433 $errstr, |
481 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; |
487 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT; |
482 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; |
488 $crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT; |
483 } |
489 } |
484 |
490 |
485 //Begin encrypted connection |
491 //Begin encrypted connection |
486 set_error_handler([$this, 'errorHandler']); |
492 set_error_handler(function () { |
|
493 call_user_func_array([$this, 'errorHandler'], func_get_args()); |
|
494 }); |
487 $crypto_ok = stream_socket_enable_crypto( |
495 $crypto_ok = stream_socket_enable_crypto( |
488 $this->smtp_conn, |
496 $this->smtp_conn, |
489 true, |
497 true, |
490 $crypto_method |
498 $crypto_method |
491 ); |
499 ); |
572 if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { |
580 if (!$this->sendCommand('AUTH', 'AUTH PLAIN', 334)) { |
573 return false; |
581 return false; |
574 } |
582 } |
575 //Send encoded username and password |
583 //Send encoded username and password |
576 if ( |
584 if ( |
577 //Format from https://tools.ietf.org/html/rfc4616#section-2 |
585 //Format from https://www.rfc-editor.org/rfc/rfc4616#section-2 |
578 //We skip the first field (it's forgery), so the string starts with a null byte |
586 //We skip the first field (it's forgery), so the string starts with a null byte |
579 !$this->sendCommand( |
587 !$this->sendCommand( |
580 'User & Password', |
588 'User & Password', |
581 base64_encode("\0" . $username . "\0" . $password), |
589 base64_encode("\0" . $username . "\0" . $password), |
582 235 |
590 235 |
646 if (function_exists('hash_hmac')) { |
654 if (function_exists('hash_hmac')) { |
647 return hash_hmac('md5', $data, $key); |
655 return hash_hmac('md5', $data, $key); |
648 } |
656 } |
649 |
657 |
650 //The following borrowed from |
658 //The following borrowed from |
651 //http://php.net/manual/en/function.mhash.php#27225 |
659 //https://www.php.net/manual/en/function.mhash.php#27225 |
652 |
660 |
653 //RFC 2104 HMAC implementation for php. |
661 //RFC 2104 HMAC implementation for php. |
654 //Creates an md5 HMAC. |
662 //Creates an md5 HMAC. |
655 //Eliminates the need to install mhash to compute a HMAC |
663 //Eliminates the need to install mhash to compute a HMAC |
656 //by Lance Rushing |
664 //by Lance Rushing |
785 $lines_out[] = $line; |
793 $lines_out[] = $line; |
786 |
794 |
787 //Send the lines to the server |
795 //Send the lines to the server |
788 foreach ($lines_out as $line_out) { |
796 foreach ($lines_out as $line_out) { |
789 //Dot-stuffing as per RFC5321 section 4.5.2 |
797 //Dot-stuffing as per RFC5321 section 4.5.2 |
790 //https://tools.ietf.org/html/rfc5321#section-4.5.2 |
798 //https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2 |
791 if (!empty($line_out) && $line_out[0] === '.') { |
799 if (!empty($line_out) && $line_out[0] === '.') { |
792 $line_out = '.' . $line_out; |
800 $line_out = '.' . $line_out; |
793 } |
801 } |
794 $this->client_send($line_out . static::LE, 'DATA'); |
802 $this->client_send($line_out . static::LE, 'DATA'); |
795 } |
803 } |
1160 ) { |
1168 ) { |
1161 $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT); |
1169 $this->edebug('CLIENT -> SERVER: [credentials hidden]', self::DEBUG_CLIENT); |
1162 } else { |
1170 } else { |
1163 $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT); |
1171 $this->edebug('CLIENT -> SERVER: ' . $data, self::DEBUG_CLIENT); |
1164 } |
1172 } |
1165 set_error_handler([$this, 'errorHandler']); |
1173 set_error_handler(function () { |
|
1174 call_user_func_array([$this, 'errorHandler'], func_get_args()); |
|
1175 }); |
1166 $result = fwrite($this->smtp_conn, $data); |
1176 $result = fwrite($this->smtp_conn, $data); |
1167 restore_error_handler(); |
1177 restore_error_handler(); |
1168 |
1178 |
1169 return $result; |
1179 return $result; |
1170 } |
1180 } |
1263 $selR = [$this->smtp_conn]; |
1273 $selR = [$this->smtp_conn]; |
1264 $selW = null; |
1274 $selW = null; |
1265 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { |
1275 while (is_resource($this->smtp_conn) && !feof($this->smtp_conn)) { |
1266 //Must pass vars in here as params are by reference |
1276 //Must pass vars in here as params are by reference |
1267 //solution for signals inspired by https://github.com/symfony/symfony/pull/6540 |
1277 //solution for signals inspired by https://github.com/symfony/symfony/pull/6540 |
1268 set_error_handler([$this, 'errorHandler']); |
1278 set_error_handler(function () { |
|
1279 call_user_func_array([$this, 'errorHandler'], func_get_args()); |
|
1280 }); |
1269 $n = stream_select($selR, $selW, $selW, $this->Timelimit); |
1281 $n = stream_select($selR, $selW, $selW, $this->Timelimit); |
1270 restore_error_handler(); |
1282 restore_error_handler(); |
1271 |
1283 |
1272 if ($n === false) { |
1284 if ($n === false) { |
1273 $message = $this->getError()['detail']; |
1285 $message = $this->getError()['detail']; |