From 2e7f8c47034b8547c46cca5c96c2ab99b0ac6bab Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Tue, 23 Jun 2026 12:29:34 +0200 Subject: [PATCH] initial --- README.md | 3 + src/AuthMailer.php | 142 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 README.md create mode 100644 src/AuthMailer.php diff --git a/README.md b/README.md new file mode 100644 index 0000000..5682604 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# ac-ms-notifications + +SMTP (post-alpha). Remote: `git://f0xx.org/ac/ac-ms-notifications` diff --git a/src/AuthMailer.php b/src/AuthMailer.php new file mode 100644 index 0000000..c6d3168 --- /dev/null +++ b/src/AuthMailer.php @@ -0,0 +1,142 @@ +'); + $replyTo = (string) cfg('mail.reply_to', ''); + $transport = strtolower((string) cfg('mail.transport', 'smtp')); + if ($transport === 'sendmail') { + return self::sendMail($to, $subject, $bodyText, $from, $replyTo); + } + return self::sendSmtp($to, $subject, $bodyText, $from, $replyTo); + } + + public static function sendVerifyEmail(string $to, string $verifyUrl): bool { + $subject = 'Verify your Android Cast account'; + $body = "Open this link to verify your email (valid 24h):\n\n" . $verifyUrl . "\n"; + return self::send($to, $subject, $body); + } + + private static function sendMail(string $to, string $subject, string $body, string $from, string $replyTo): bool { + $headers = "From: {$from}\r\n"; + if ($replyTo !== '') { + $headers .= "Reply-To: {$replyTo}\r\n"; + } + $headers .= "Content-Type: text/plain; charset=UTF-8\r\n"; + return @mail($to, $subject, $body, $headers); + } + + private static function sendSmtp(string $to, string $subject, string $body, string $from, string $replyTo): bool { + $host = (string) cfg('mail.smtp.host', '127.0.0.1'); + $port = (int) cfg('mail.smtp.port', 587); + $enc = strtolower((string) cfg('mail.smtp.encryption', 'tls')); + $user = (string) cfg('mail.smtp.username', ''); + $pass = (string) cfg('mail.smtp.password', ''); + $remote = ($enc === 'ssl' ? 'ssl://' : '') . $host; + $errno = 0; + $errstr = ''; + $fp = @stream_socket_client($remote . ':' . $port, $errno, $errstr, 15); + if ($fp === false) { + error_log('AuthMailer SMTP connect failed: ' . $errstr); + return self::sendMail($to, $subject, $body, $from, $replyTo); + } + stream_set_timeout($fp, 15); + if (!self::smtpExpect($fp, [220])) { + fclose($fp); + return false; + } + $ehloHost = 'localhost'; + fwrite($fp, "EHLO {$ehloHost}\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + if ($enc === 'tls') { + fwrite($fp, "STARTTLS\r\n"); + if (!self::smtpExpect($fp, [220])) { + fclose($fp); + return false; + } + if (!stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) { + fclose($fp); + return false; + } + fwrite($fp, "EHLO {$ehloHost}\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + } + if ($user !== '') { + fwrite($fp, "AUTH LOGIN\r\n"); + if (!self::smtpExpect($fp, [334])) { + fclose($fp); + return false; + } + fwrite($fp, base64_encode($user) . "\r\n"); + if (!self::smtpExpect($fp, [334])) { + fclose($fp); + return false; + } + fwrite($fp, base64_encode($pass) . "\r\n"); + if (!self::smtpExpect($fp, [235])) { + fclose($fp); + return false; + } + } + $fromAddr = self::extractAddress($from); + fwrite($fp, "MAIL FROM:<{$fromAddr}>\r\n"); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + fwrite($fp, "RCPT TO:<{$to}>\r\n"); + if (!self::smtpExpect($fp, [250, 251])) { + fclose($fp); + return false; + } + fwrite($fp, "DATA\r\n"); + if (!self::smtpExpect($fp, [354])) { + fclose($fp); + return false; + } + $msg = "From: {$from}\r\n"; + if ($replyTo !== '') { + $msg .= "Reply-To: {$replyTo}\r\n"; + } + $msg .= "To: {$to}\r\nSubject: {$subject}\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n{$body}\r\n.\r\n"; + fwrite($fp, $msg); + if (!self::smtpExpect($fp, [250])) { + fclose($fp); + return false; + } + fwrite($fp, "QUIT\r\n"); + fclose($fp); + return true; + } + + /** @param resource $fp @param list $codes */ + private static function smtpExpect($fp, array $codes): bool { + $line = ''; + while (($chunk = fgets($fp, 512)) !== false) { + $line .= $chunk; + if (strlen($chunk) >= 4 && $chunk[3] === ' ') { + break; + } + } + $code = (int) substr(trim($line), 0, 3); + return in_array($code, $codes, true); + } + + private static function extractAddress(string $from): string { + if (preg_match('/<([^>]+)>/', $from, $m)) { + return trim($m[1]); + } + return trim($from); + } +}