01de7ed4fb
Add mail.php with STARTTLS sending, test scripts, server.env split from local .env, admin@ postbox docs, DKIM DNS support-ticket notes, and todo 11 for boka relay. Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
4.1 KiB
PHP
148 lines
4.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__ . '/bootstrap.php';
|
|
|
|
/**
|
|
* Send mail via SMTP (AUTH LOGIN + STARTTLS).
|
|
* Uses SMTP_* and SMTP_FROM from private/.env.
|
|
*/
|
|
function sendMail(string $to, string $subject, string $body, ?string $from = null): void
|
|
{
|
|
$host = env('SMTP_HOST');
|
|
$port = (int) (env('SMTP_PORT') ?: 587);
|
|
$user = env('SMTP_USER');
|
|
$pass = env('SMTP_PASSWORD');
|
|
$from = $from ?? env('SMTP_FROM') ?? $user;
|
|
|
|
if (!$host || !$user || !$pass || !$from) {
|
|
throw new RuntimeException('SMTP not configured — set SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_FROM in .env');
|
|
}
|
|
|
|
$encryption = strtolower(env('SMTP_ENCRYPTION') ?? 'tls');
|
|
$socket = connectSmtp($host, $port, $encryption);
|
|
|
|
$fromHeader = $from ?? env('SMTP_FROM') ?? $user;
|
|
// Envelope sender must be the authenticated mailbox (aliases are header-only on Plesk).
|
|
$envelopeFrom = $user;
|
|
|
|
try {
|
|
expectCode($socket, 220);
|
|
sendCommand($socket, 'EHLO ' . gethostname(), 250);
|
|
|
|
if ($encryption === 'tls') {
|
|
sendCommand($socket, 'STARTTLS', 220);
|
|
if (!stream_socket_enable_crypto($socket, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
|
|
throw new RuntimeException('STARTTLS failed');
|
|
}
|
|
sendCommand($socket, 'EHLO ' . gethostname(), 250);
|
|
}
|
|
|
|
sendCommand($socket, 'AUTH LOGIN', 334);
|
|
sendCommand($socket, base64_encode($user), 334);
|
|
sendCommand($socket, base64_encode($pass), 235);
|
|
|
|
sendCommand($socket, 'MAIL FROM:<' . $envelopeFrom . '>', 250);
|
|
sendCommand($socket, 'RCPT TO:<' . $to . '>', 250);
|
|
sendCommand($socket, 'DATA', 354);
|
|
|
|
$message = buildMessage($fromHeader, $to, $subject, $body);
|
|
fwrite($socket, $message);
|
|
expectCode($socket, 250);
|
|
|
|
sendCommand($socket, 'QUIT', 221);
|
|
} finally {
|
|
fclose($socket);
|
|
}
|
|
}
|
|
|
|
/** @return resource */
|
|
function connectSmtp(string $host, int $port, string $encryption)
|
|
{
|
|
$target = $encryption === 'ssl'
|
|
? "ssl://{$host}:{$port}"
|
|
: "tcp://{$host}:{$port}";
|
|
|
|
$socket = @stream_socket_client(
|
|
$target,
|
|
$errno,
|
|
$errstr,
|
|
30,
|
|
STREAM_CLIENT_CONNECT,
|
|
stream_context_create(['ssl' => ['verify_peer' => true, 'verify_peer_name' => true]])
|
|
);
|
|
|
|
if ($socket === false) {
|
|
throw new RuntimeException("SMTP connect failed: {$errstr} ({$errno})");
|
|
}
|
|
|
|
stream_set_timeout($socket, 30);
|
|
|
|
return $socket;
|
|
}
|
|
|
|
/** @param resource $socket */
|
|
function sendCommand($socket, string $command, int $expectedCode): void
|
|
{
|
|
fwrite($socket, $command . "\r\n");
|
|
expectCode($socket, $expectedCode);
|
|
}
|
|
|
|
/** @param resource $socket */
|
|
function expectCode($socket, int $expectedCode): void
|
|
{
|
|
$response = readResponse($socket);
|
|
$code = (int) substr($response, 0, 3);
|
|
|
|
if ($code !== $expectedCode) {
|
|
throw new RuntimeException("SMTP error (expected {$expectedCode}): {$response}");
|
|
}
|
|
}
|
|
|
|
/** @param resource $socket */
|
|
function readResponse($socket): string
|
|
{
|
|
$response = '';
|
|
|
|
while (($line = fgets($socket, 515)) !== false) {
|
|
$response .= $line;
|
|
if (isset($line[3]) && $line[3] === ' ') {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return trim($response);
|
|
}
|
|
|
|
function buildMessage(string $from, string $to, string $subject, string $body): string
|
|
{
|
|
$domain = env('DOMAIN', '0012.de');
|
|
$date = gmdate('D, d M Y H:i:s') . ' +0000';
|
|
$messageId = sprintf('<%s@%s>', bin2hex(random_bytes(8)), $domain);
|
|
|
|
$headers = [
|
|
"From: {$from}",
|
|
"To: {$to}",
|
|
"Subject: {$subject}",
|
|
"Date: {$date}",
|
|
"Message-ID: {$messageId}",
|
|
'MIME-Version: 1.0',
|
|
'Content-Type: text/plain; charset=UTF-8',
|
|
'Content-Transfer-Encoding: 8bit',
|
|
];
|
|
|
|
return implode("\r\n", $headers) . "\r\n\r\n" . $body . "\r\n.\r\n";
|
|
}
|
|
|
|
function sendAlert(string $subject, string $body): void
|
|
{
|
|
$to = env('ALERT_EMAIL');
|
|
|
|
if (!$to) {
|
|
throw new RuntimeException('ALERT_EMAIL not set in .env');
|
|
}
|
|
|
|
sendMail($to, $subject, $body);
|
|
}
|