mirror of
git://f0xx.org/ac/ac-ms-notifications
synced 2026-07-29 04:17:36 +03:00
initial
This commit is contained in:
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# ac-ms-notifications
|
||||
|
||||
SMTP (post-alpha). Remote: `git://f0xx.org/ac/ac-ms-notifications`
|
||||
142
src/AuthMailer.php
Normal file
142
src/AuthMailer.php
Normal file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/** Outbound mail (registration verify, password reset). Task 2.5 — uses config mail.* */
|
||||
final class AuthMailer {
|
||||
private function __construct() {
|
||||
}
|
||||
|
||||
public static function send(string $to, string $subject, string $bodyText): bool {
|
||||
$from = (string) cfg('mail.from', 'Android Cast <noreply@apps.f0xx.org>');
|
||||
$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<int> $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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user