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>
This commit is contained in:
+147
@@ -0,0 +1,147 @@
|
||||
<?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);
|
||||
}
|
||||
Reference in New Issue
Block a user