|
|
@@ -0,0 +1,426 @@
|
|
|
+<?php
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+// ─────────────────────────────────────────────────────────────────────────────
|
|
|
+// CONFIGURATION — set the access password here.
|
|
|
+// Every visitor must enter this before they can send a test mail.
|
|
|
+// Set it to '' (empty string) to disable the password gate entirely.
|
|
|
+// ─────────────────────────────────────────────────────────────────────────────
|
|
|
+const ACCESS_PASSWORD = 'asdf123!';
|
|
|
+
|
|
|
+const SENDER_ADDRESS = 'test@med0.de';
|
|
|
+const SENDER_NAME = 'med0.de Mail Test';
|
|
|
+const MIN_FORM_SECONDS = 3; // reject sends submitted faster than a human could type
|
|
|
+const HONEYPOT_FIELD = 'website'; // hidden decoy field; if filled, the sender is a bot
|
|
|
+
|
|
|
+const SMTP_TIMEOUT = 15; // seconds per SMTP step
|
|
|
+const EHLO_NAME = 'tool.medowar.de'; // name announced in EHLO
|
|
|
+
|
|
|
+/**
|
|
|
+ * Builds the fixed test message. Returns the subject, body and an ordered map of
|
|
|
+ * header name => value. The subject and body are predefined — only the recipient
|
|
|
+ * and (optionally) the target server vary.
|
|
|
+ *
|
|
|
+ * Mail hygiene is kept deliberately lightweight but complete: a real From with
|
|
|
+ * display name, matching Reply-To, a unique Message-ID, Date, correct MIME
|
|
|
+ * headers, and Auto-Submitted so downstream systems know it is machine-generated
|
|
|
+ * and should not auto-reply. The envelope sender is always SENDER_ADDRESS, which
|
|
|
+ * is what SPF evaluates.
|
|
|
+ *
|
|
|
+ * @return array{subject:string, body:string, headers:array<string,string>}
|
|
|
+ */
|
|
|
+function buildMessage(string $selfUrl): array
|
|
|
+{
|
|
|
+ $now = new DateTimeImmutable('now', new DateTimeZone('UTC'));
|
|
|
+
|
|
|
+ // Subject stays ASCII so it needs no MIME encoded-word wrapping.
|
|
|
+ $subject = 'Test message from the med0.de mail tools';
|
|
|
+
|
|
|
+ $body =
|
|
|
+ "Hello,\r\n" .
|
|
|
+ "\r\n" .
|
|
|
+ "This is an automated TEST message sent by the med0.de mail-testing tools.\r\n" .
|
|
|
+ "If you received it, delivery from " . SENDER_ADDRESS . " to your address is working.\r\n" .
|
|
|
+ "You can safely ignore or delete this message — no action is required.\r\n" .
|
|
|
+ "\r\n" .
|
|
|
+ "Test page: " . $selfUrl . "\r\n" .
|
|
|
+ "Sent (UTC): " . $now->format('Y-m-d H:i:s') . "\r\n" .
|
|
|
+ "\r\n" .
|
|
|
+ "— med0.de mail tools\r\n";
|
|
|
+
|
|
|
+ // A unique, domain-scoped Message-ID aids threading and spam scoring.
|
|
|
+ $messageId = sprintf('<%s.%s@med0.de>', $now->format('YmdHis'), bin2hex(random_bytes(8)));
|
|
|
+
|
|
|
+ $headers = [
|
|
|
+ 'From' => sprintf('%s <%s>', SENDER_NAME, SENDER_ADDRESS),
|
|
|
+ 'Reply-To' => SENDER_ADDRESS,
|
|
|
+ 'Message-ID' => $messageId,
|
|
|
+ 'Date' => $now->format(DateTimeInterface::RFC2822),
|
|
|
+ 'MIME-Version' => '1.0',
|
|
|
+ 'Content-Type' => 'text/plain; charset=UTF-8',
|
|
|
+ 'Content-Transfer-Encoding' => '8bit',
|
|
|
+ 'Auto-Submitted' => 'auto-generated', // RFC 3834: do not auto-reply
|
|
|
+ 'X-Mailer' => 'med0.de-test-tool',
|
|
|
+ ];
|
|
|
+
|
|
|
+ return ['subject' => $subject, 'body' => $body, 'headers' => $headers];
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Dispatches the test message. With $mailserver empty the message goes through
|
|
|
+ * PHP's mail() (the local MTA does the usual MX routing). With $mailserver set it
|
|
|
+ * is delivered by talking SMTP directly to that host — no local MTA involved.
|
|
|
+ *
|
|
|
+ * @param string[] $transcript filled with the SMTP conversation (direct mode only)
|
|
|
+ */
|
|
|
+function sendTestMail(string $recipient, string $selfUrl, ?string $mailserver, ?string &$error, ?array &$transcript): bool
|
|
|
+{
|
|
|
+ $error = null;
|
|
|
+ $transcript = [];
|
|
|
+ $msg = buildMessage($selfUrl);
|
|
|
+
|
|
|
+ if ($mailserver === null || $mailserver === '') {
|
|
|
+ // Normal path: hand off to the local MTA. The 5th parameter sets the
|
|
|
+ // envelope sender (Return-Path) so SPF checks the med0.de domain.
|
|
|
+ $headerLines = [];
|
|
|
+ foreach ($msg['headers'] as $k => $v) {
|
|
|
+ $headerLines[] = $k . ': ' . $v;
|
|
|
+ }
|
|
|
+ $ok = @mail($recipient, $msg['subject'], $msg['body'], implode("\r\n", $headerLines), '-f' . SENDER_ADDRESS);
|
|
|
+ if (!$ok) {
|
|
|
+ $error = 'The local mail server rejected or failed to accept the message. '
|
|
|
+ . 'Check that this host is configured to send mail for med0.de.';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Direct path: parse host[:port] (default 25) and speak SMTP to that server.
|
|
|
+ $host = $mailserver;
|
|
|
+ $port = 25;
|
|
|
+ if (preg_match('/^(.+):(\d+)$/', $mailserver, $m)) {
|
|
|
+ $host = $m[1];
|
|
|
+ $port = (int) $m[2];
|
|
|
+ }
|
|
|
+
|
|
|
+ // Assemble the full RFC 5322 message (To/Subject go inside DATA here).
|
|
|
+ $lines = ['To: ' . $recipient, 'Subject: ' . $msg['subject']];
|
|
|
+ foreach ($msg['headers'] as $k => $v) {
|
|
|
+ $lines[] = $k . ': ' . $v;
|
|
|
+ }
|
|
|
+ $raw = implode("\r\n", $lines) . "\r\n\r\n" . $msg['body'];
|
|
|
+
|
|
|
+ return smtpDeliver($host, $port, SENDER_ADDRESS, $recipient, $raw, $error, $transcript);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Minimal SMTP client: connects, EHLOs, opportunistically upgrades with STARTTLS
|
|
|
+ * when offered, then MAIL FROM / RCPT TO / DATA. No AUTH — this mirrors how an MTA
|
|
|
+ * delivers straight to a recipient's mail exchanger on port 25.
|
|
|
+ *
|
|
|
+ * @param string[] $transcript
|
|
|
+ */
|
|
|
+function smtpDeliver(string $host, int $port, string $from, string $to, string $rawMessage, ?string &$error, array &$transcript): bool
|
|
|
+{
|
|
|
+ $errno = 0; $errstr = '';
|
|
|
+ $stream = @stream_socket_client(
|
|
|
+ sprintf('tcp://%s:%d', $host, $port), $errno, $errstr,
|
|
|
+ SMTP_TIMEOUT, STREAM_CLIENT_CONNECT
|
|
|
+ );
|
|
|
+ if (!is_resource($stream)) {
|
|
|
+ $error = $errstr !== '' ? "Connection to $host:$port failed: $errstr (errno $errno)" : 'Connection failed.';
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ stream_set_timeout($stream, SMTP_TIMEOUT);
|
|
|
+
|
|
|
+ // Reads a full (possibly multiline) reply and asserts the leading status code.
|
|
|
+ $expect = function (string $code) use ($stream, &$transcript, &$error): bool {
|
|
|
+ $status = '';
|
|
|
+ while (($line = fgets($stream, 4096)) !== false) {
|
|
|
+ $transcript[] = 'S: ' . rtrim($line, "\r\n");
|
|
|
+ $status = substr($line, 0, 3);
|
|
|
+ if (strlen($line) < 4 || $line[3] !== '-') { // last line has a space, not '-'
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (strpos($status, $code) !== 0) {
|
|
|
+ $error = sprintf('Expected %s but server said: %s', $code, trim($status . ' ...'));
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ };
|
|
|
+ $send = function (string $cmd) use ($stream, &$transcript): void {
|
|
|
+ $transcript[] = 'C: ' . $cmd;
|
|
|
+ fwrite($stream, $cmd . "\r\n");
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ if (!$expect('220')) { throw new RuntimeException($error); }
|
|
|
+ $send('EHLO ' . EHLO_NAME);
|
|
|
+
|
|
|
+ // Capture EHLO capabilities to decide whether STARTTLS is offered.
|
|
|
+ $caps = '';
|
|
|
+ while (($line = fgets($stream, 4096)) !== false) {
|
|
|
+ $transcript[] = 'S: ' . rtrim($line, "\r\n");
|
|
|
+ $caps .= $line;
|
|
|
+ if (strlen($line) < 4 || $line[3] !== '-') { break; }
|
|
|
+ }
|
|
|
+ if (strpos($caps, '250') !== 0) {
|
|
|
+ throw new RuntimeException('EHLO was rejected by ' . $host . '.');
|
|
|
+ }
|
|
|
+
|
|
|
+ // Opportunistic STARTTLS — encrypt when offered, but do not require a
|
|
|
+ // trusted certificate (recipient MXs routinely use self-signed certs).
|
|
|
+ if (stripos($caps, 'STARTTLS') !== false) {
|
|
|
+ $send('STARTTLS');
|
|
|
+ if (!$expect('220')) { throw new RuntimeException($error); }
|
|
|
+ stream_context_set_option($stream, 'ssl', 'verify_peer', false);
|
|
|
+ stream_context_set_option($stream, 'ssl', 'verify_peer_name', false);
|
|
|
+ stream_context_set_option($stream, 'ssl', 'allow_self_signed', true);
|
|
|
+ if (@stream_socket_enable_crypto($stream, true, STREAM_CRYPTO_METHOD_TLS_CLIENT) !== true) {
|
|
|
+ throw new RuntimeException('STARTTLS negotiation failed with ' . $host . '.');
|
|
|
+ }
|
|
|
+ $transcript[] = '* TLS established';
|
|
|
+ $send('EHLO ' . EHLO_NAME); // RFC 3207: re-issue EHLO after the upgrade
|
|
|
+ if (!$expect('250')) { throw new RuntimeException($error); }
|
|
|
+ }
|
|
|
+
|
|
|
+ $send('MAIL FROM:<' . $from . '>');
|
|
|
+ if (!$expect('250')) { throw new RuntimeException($error); }
|
|
|
+ $send('RCPT TO:<' . $to . '>');
|
|
|
+ if (!$expect('25')) { throw new RuntimeException($error); } // 250 or 251
|
|
|
+ $send('DATA');
|
|
|
+ if (!$expect('354')) { throw new RuntimeException($error); }
|
|
|
+
|
|
|
+ // Dot-stuff any line that begins with '.' then terminate with <CRLF>.<CRLF>.
|
|
|
+ $data = preg_replace('/^\./m', '..', $rawMessage);
|
|
|
+ $transcript[] = 'C: [message data, ' . strlen($data) . ' bytes]';
|
|
|
+ fwrite($stream, $data . "\r\n.\r\n");
|
|
|
+ if (!$expect('250')) { throw new RuntimeException($error); }
|
|
|
+
|
|
|
+ $send('QUIT');
|
|
|
+ $expect('221'); // best-effort; delivery already accepted above
|
|
|
+ fclose($stream);
|
|
|
+ return true;
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ @fclose($stream);
|
|
|
+ if ($error === null || $error === '') {
|
|
|
+ $error = $e->getMessage();
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+session_start();
|
|
|
+
|
|
|
+// ── Access gate ──────────────────────────────────────────────────────────────
|
|
|
+// A correct password unlocks the tool for the rest of the session.
|
|
|
+$gateEnabled = (ACCESS_PASSWORD !== '');
|
|
|
+$authed = !$gateEnabled || !empty($_SESSION['stm_authed']);
|
|
|
+$loginError = null;
|
|
|
+$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
|
|
|
+
|
|
|
+// The login form carries only "password"; the send form carries "recipient".
|
|
|
+$isLoginAttempt = $isPost && isset($_POST['password']) && !isset($_POST['recipient']);
|
|
|
+if (!$authed && $isLoginAttempt) {
|
|
|
+ if (hash_equals(ACCESS_PASSWORD, (string) $_POST['password'])) {
|
|
|
+ session_regenerate_id(true); // fresh id on privilege change (fixation defence)
|
|
|
+ $_SESSION['stm_authed'] = true;
|
|
|
+ $authed = true;
|
|
|
+ } else {
|
|
|
+ $loginError = 'Incorrect password.';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Build an absolute URL to this page, used in the mail body and the form action.
|
|
|
+$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
|
|
+$host = $_SERVER['HTTP_HOST'] ?? 'tool.medowar.de';
|
|
|
+$path = strtok($_SERVER['REQUEST_URI'] ?? '/send-test-mail.php', '?');
|
|
|
+$selfUrl = $scheme . '://' . $host . $path;
|
|
|
+
|
|
|
+$recipient = trim((string) ($_POST['recipient'] ?? ''));
|
|
|
+$mailserver = trim((string) ($_POST['mailserver'] ?? ''));
|
|
|
+$sent = false;
|
|
|
+$sendError = null;
|
|
|
+$inputError = null;
|
|
|
+$transcript = [];
|
|
|
+
|
|
|
+$isSendAttempt = $authed && $isPost && isset($_POST['recipient']);
|
|
|
+if ($isSendAttempt) {
|
|
|
+ $honeypot = trim((string) ($_POST[HONEYPOT_FIELD] ?? ''));
|
|
|
+ $formTs = (int) ($_SESSION['stm_form_ts'] ?? 0);
|
|
|
+ $elapsed = time() - $formTs;
|
|
|
+
|
|
|
+ if ($honeypot !== '') {
|
|
|
+ // (1) Honeypot: a hidden field only a bot would fill. Feign success, send nothing.
|
|
|
+ $sent = true;
|
|
|
+ } elseif ($formTs === 0 || $elapsed < MIN_FORM_SECONDS) {
|
|
|
+ // (2) Time-trap: nobody fills and submits the form this fast — likely a bot.
|
|
|
+ $inputError = 'That was submitted a little too quickly — please try again.';
|
|
|
+ } else {
|
|
|
+ // FILTER_VALIDATE_EMAIL also rejects CR/LF, closing the header-injection door.
|
|
|
+ $clean = filter_var($recipient, FILTER_VALIDATE_EMAIL);
|
|
|
+ if ($clean === false) {
|
|
|
+ $inputError = 'Please enter a valid recipient email address.';
|
|
|
+ } elseif ($mailserver !== '' && !preg_match('/^(?:[a-z0-9](?:-?[a-z0-9])*\.)+[a-z]{2,}(?::\d{1,5})?$|^\d{1,3}(?:\.\d{1,3}){3}(?::\d{1,5})?$/i', $mailserver)) {
|
|
|
+ $inputError = 'Please enter a valid mailserver as host or host:port (or leave it blank for normal MX delivery).';
|
|
|
+ } else {
|
|
|
+ $recipient = $clean;
|
|
|
+ $sent = sendTestMail($recipient, $selfUrl, $mailserver !== '' ? $mailserver : null, $sendError, $transcript);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// Stamp a fresh render time so the next send has a timing baseline to check against.
|
|
|
+if ($authed) {
|
|
|
+ $_SESSION['stm_form_ts'] = time();
|
|
|
+}
|
|
|
+?>
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="en">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>Send Test Mail</title>
|
|
|
+ <style>
|
|
|
+ body {
|
|
|
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
+ max-width: 1000px;
|
|
|
+ margin: 40px auto;
|
|
|
+ padding: 0 20px;
|
|
|
+ background: #f5f5f5;
|
|
|
+ color: #333;
|
|
|
+ }
|
|
|
+ h1 { color: #333; }
|
|
|
+ .info { background: #e3f2fd; padding: 15px; border-radius: 5px; margin-bottom: 20px; }
|
|
|
+ form.lookup { margin-bottom: 20px; display: flex; gap: 10px; flex-wrap: wrap; align-items: stretch; }
|
|
|
+ input[type=email], input[type=password], input[type=text] {
|
|
|
+ padding: 10px; font-size: 15px; border: 1px solid #ccc; border-radius: 5px; background: white; flex: 1; min-width: 220px;
|
|
|
+ }
|
|
|
+ details.transcript { margin: 16px 0; }
|
|
|
+ details.transcript pre {
|
|
|
+ background: #263238; color: #cfd8dc; padding: 12px 14px; border-radius: 5px;
|
|
|
+ font-size: 12px; overflow-x: auto; line-height: 1.5;
|
|
|
+ }
|
|
|
+ details.transcript summary { cursor: pointer; font-weight: 600; color: #1976D2; }
|
|
|
+ /* Honeypot: kept in the layout for bots but invisible and unfocusable for humans. */
|
|
|
+ .hp { position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
|
|
+ .btn {
|
|
|
+ display: inline-block; padding: 10px 20px; background: #2196F3; color: white;
|
|
|
+ text-decoration: none; border-radius: 5px; border: none; cursor: pointer; font-size: 15px;
|
|
|
+ }
|
|
|
+ .btn:hover { background: #1976D2; }
|
|
|
+ .error { background: #ffebee; color: #c62828; padding: 10px; border-radius: 5px; margin: 6px 0; }
|
|
|
+ .verdict {
|
|
|
+ display: flex; align-items: center; gap: 12px; padding: 16px 20px; border-radius: 6px;
|
|
|
+ font-size: 18px; font-weight: 600; margin: 16px 0;
|
|
|
+ }
|
|
|
+ .verdict.ok { background: #e8f5e9; color: #2e7d32; border-left: 6px solid #43a047; }
|
|
|
+ .verdict.fail { background: #ffebee; color: #c62828; border-left: 6px solid #e53935; }
|
|
|
+ h2 { color: #333; margin-top: 30px; }
|
|
|
+ table { width: 100%; border-collapse: collapse; background: white; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-top: 10px; }
|
|
|
+ th, td { padding: 10px 12px; text-align: left; border-bottom: 1px solid #eee; font-size: 13px; vertical-align: top; }
|
|
|
+ th { width: 180px; color: #555; background: #fafafa; font-weight: 600; }
|
|
|
+ td.mono, .mono { font-family: monospace; word-break: break-all; }
|
|
|
+ pre.preview {
|
|
|
+ background: #263238; color: #cfd8dc; padding: 12px 14px; border-radius: 5px;
|
|
|
+ font-size: 12px; overflow-x: auto; line-height: 1.5; white-space: pre-wrap; word-break: break-word;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <h1>✉️ Send Test Mail</h1>
|
|
|
+
|
|
|
+ <?php if (!$authed): ?>
|
|
|
+ <div class="info">🔒 This tool is password-protected. Enter the password to continue.</div>
|
|
|
+ <?php if ($loginError): ?>
|
|
|
+ <div class="error"><?= htmlspecialchars($loginError) ?></div>
|
|
|
+ <?php endif; ?>
|
|
|
+ <form class="lookup" method="POST" action="<?= htmlspecialchars($selfUrl) ?>">
|
|
|
+ <input type="password" name="password" placeholder="Password" required autofocus>
|
|
|
+ <button type="submit" class="btn">🔓 Unlock</button>
|
|
|
+ </form>
|
|
|
+ <?php else: ?>
|
|
|
+
|
|
|
+ <div class="info">
|
|
|
+ Sends a fixed, clearly-labelled <strong>test message</strong> from
|
|
|
+ <code><?= htmlspecialchars(SENDER_ADDRESS) ?></code> to the address you enter.
|
|
|
+ The subject and body are predefined — the only input is the recipient. Use it to
|
|
|
+ confirm that outbound delivery for <code>med0.de</code> reaches a given mailbox.
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <form class="lookup" method="POST" action="<?= htmlspecialchars($selfUrl) ?>">
|
|
|
+ <input type="email" name="recipient" placeholder="recipient@example.com"
|
|
|
+ value="<?= htmlspecialchars($recipient) ?>" required autofocus>
|
|
|
+ <input type="text" name="mailserver" placeholder="mailserver (optional, e.g. mx.example.com:25)"
|
|
|
+ value="<?= htmlspecialchars($mailserver) ?>">
|
|
|
+ <!-- Honeypot: humans never see or fill this; bots that auto-fill forms do. -->
|
|
|
+ <div class="hp" aria-hidden="true">
|
|
|
+ <label>Leave this field empty
|
|
|
+ <input type="text" name="<?= htmlspecialchars(HONEYPOT_FIELD) ?>" tabindex="-1" autocomplete="off">
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ <button type="submit" class="btn">✉️ Send test mail</button>
|
|
|
+ </form>
|
|
|
+ <p style="color:#888;font-size:12px;margin-top:-10px;">
|
|
|
+ Leave <strong>mailserver</strong> empty to deliver via the local mail system (normal MX routing).
|
|
|
+ Fill it to deliver <strong>directly</strong> to that host over SMTP (default port 25, opportunistic STARTTLS).
|
|
|
+ </p>
|
|
|
+
|
|
|
+ <?php if ($inputError): ?>
|
|
|
+ <div class="error"><?= htmlspecialchars($inputError) ?></div>
|
|
|
+ <?php endif; ?>
|
|
|
+
|
|
|
+ <?php if ($isSendAttempt && !$inputError): ?>
|
|
|
+ <?php $direct = ($mailserver !== ''); ?>
|
|
|
+ <?php if ($sent): ?>
|
|
|
+ <div class="verdict ok">✅ Test message
|
|
|
+ <?= $direct
|
|
|
+ ? 'accepted by ' . htmlspecialchars($mailserver)
|
|
|
+ : 'handed off to the local mail server' ?>
|
|
|
+ for <?= htmlspecialchars($recipient) ?>.</div>
|
|
|
+ <p style="color:#888;font-size:13px;">
|
|
|
+ <?= $direct
|
|
|
+ ? 'The target server accepted the message for delivery.'
|
|
|
+ : 'A successful hand-off means the local mail system accepted the message — it does not guarantee final delivery.' ?>
|
|
|
+ Check the recipient's inbox (and spam folder).
|
|
|
+ </p>
|
|
|
+ <?php else: ?>
|
|
|
+ <div class="verdict fail">❌ Could not send the test message.</div>
|
|
|
+ <?php if ($sendError): ?>
|
|
|
+ <div class="error"><?= htmlspecialchars($sendError) ?></div>
|
|
|
+ <?php endif; ?>
|
|
|
+ <?php endif; ?>
|
|
|
+ <?php if (!empty($transcript)): ?>
|
|
|
+ <details class="transcript" open>
|
|
|
+ <summary>SMTP conversation with <?= htmlspecialchars($mailserver) ?></summary>
|
|
|
+ <pre><?= htmlspecialchars(implode("\n", $transcript)) ?></pre>
|
|
|
+ </details>
|
|
|
+ <?php endif; ?>
|
|
|
+ <?php endif; ?>
|
|
|
+
|
|
|
+ <h2>What gets sent</h2>
|
|
|
+ <table>
|
|
|
+ <tr><th>From</th><td class="mono"><?= htmlspecialchars(SENDER_NAME . ' <' . SENDER_ADDRESS . '>') ?></td></tr>
|
|
|
+ <tr><th>Reply-To</th><td class="mono"><?= htmlspecialchars(SENDER_ADDRESS) ?></td></tr>
|
|
|
+ <tr><th>Subject</th><td class="mono">Test message from the med0.de mail tools</td></tr>
|
|
|
+ <tr>
|
|
|
+ <th>Body</th>
|
|
|
+ <td>
|
|
|
+<pre class="preview">Hello,
|
|
|
+
|
|
|
+This is an automated TEST message sent by the med0.de mail-testing tools.
|
|
|
+If you received it, delivery from <?= htmlspecialchars(SENDER_ADDRESS) ?> to your address is working.
|
|
|
+You can safely ignore or delete this message — no action is required.
|
|
|
+
|
|
|
+Test page: <?= htmlspecialchars($selfUrl) ?>
|
|
|
+
|
|
|
+Sent (UTC): …
|
|
|
+
|
|
|
+— med0.de mail tools</pre>
|
|
|
+ </td>
|
|
|
+ </tr>
|
|
|
+ </table>
|
|
|
+ <?php endif; ?>
|
|
|
+</body>
|
|
|
+</html>
|