Files
0012/config/mail.php
T
Frank Schwenk 01de7ed4fb feat: SMTP mail stack and separate server.env deploy
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>
2026-06-11 23:19:03 +02:00

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);
}