<?php
/**
* SMTP Tester UI (single-file)
* - Web UI to test Microsoft SMTP Relay (port 25) and/or Submission (587)
* - Shows local server IPs and (optional) external IP
* - Logs full SMTP dialog
*/
declare(strict_types=1);
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function getServerAddr(): string {
return $_SERVER['SERVER_ADDR'] ?? gethostbyname(gethostname());
}
function getLocalInterfaceIps(): array {
$ips = [];
$host = gethostname();
if ($host) {
$ip = gethostbyname($host);
if ($ip && $ip !== $host) $ips[] = $ip;
}
// Best-effort shell commands (may be disabled)
$cmds = [
"ip -o -4 addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1",
"ifconfig 2>/dev/null | grep -Eo 'inet (addr:)?([0-9]{1,3}\\.){3}[0-9]{1,3}' | grep -Eo '([0-9]{1,3}\\.){3}[0-9]{1,3}'",
"hostname -I 2>/dev/null",
];
foreach ($cmds as $cmd) {
$res = @shell_exec($cmd);
if ($res) {
foreach (preg_split('/\s+/', trim($res)) as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) $ips[] = $ip;
}
}
}
$ips = array_values(array_unique(array_filter($ips, fn($ip) => $ip !== "127.0.0.1")));
return $ips;
}
function getExternalIp(): ?string {
// Optional; only if outbound HTTPS allowed
$services = [
"https://api.ipify.org",
"https://checkip.amazonaws.com",
"https://ifconfig.me/ip",
];
foreach ($services as $url) {
$ctx = stream_context_create([
'http' => ['timeout' => 4],
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
$ip = @file_get_contents($url, false, $ctx);
$ip = trim((string)$ip);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) return $ip;
}
return null;
}
function smtpReadLine($fp): string {
$line = fgets($fp, 8192);
return $line === false ? "" : rtrim($line, "\r\n");
}
function smtpReadResponse($fp): array {
$lines = [];
while (!feof($fp)) {
$line = smtpReadLine($fp);
if ($line === "") break;
$lines[] = $line;
// End of multi-line response: "250 <text>" (4th char is space)
if (strlen($line) >= 4 && $line[3] === ' ') break;
}
return $lines;
}
function smtpSend($fp, string $cmd): void {
fwrite($fp, $cmd . "\r\n");
}
function smtpExpect(array $respLines, array $allowedFirstDigits = ['2','3']): bool {
foreach ($respLines as $line) {
if (preg_match('/^(\d{3})\b/', $line, $m)) {
return in_array($m[1][0], $allowedFirstDigits, true);
}
}
return false;
}
function smtpTest(array $cfg): string {
$log = [];
$add = function(string $s) use (&$log) { $log[] = $s; };
$add(str_repeat("-", 78));
$add("TEST: {$cfg['name']}");
$add("Target: {$cfg['host']}:{$cfg['port']} | STARTTLS=" . ($cfg['starttls'] ? "yes" : "no") .
" | AUTH=" . ($cfg['auth'] ? "yes" : "no") .
" | SENDMAIL=" . ($cfg['sendMail'] ? "yes" : "no"));
$add(str_repeat("-", 78));
if (empty($cfg['host'])) {
$add("❌ Host ist leer.");
return implode("\n", $log);
}
$remote = "tcp://{$cfg['host']}:{$cfg['port']}";
$fp = @stream_socket_client($remote, $errno, $errstr, (int)$cfg['timeout'], STREAM_CLIENT_CONNECT);
if (!$fp) {
$add("❌ Connection failed: [{$errno}] {$errstr}");
return implode("\n", $log);
}
stream_set_timeout($fp, (int)$cfg['timeout']);
// Greeting
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) {
$add("❌ Bad greeting.");
fclose($fp);
return implode("\n", $log);
}
// EHLO/HELO
smtpSend($fp, "EHLO {$cfg['helo']}");
$add("C: EHLO {$cfg['helo']}");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) {
smtpSend($fp, "HELO {$cfg['helo']}");
$add("C: HELO {$cfg['helo']}");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) {
$add("❌ EHLO/HELO failed.");
fclose($fp);
return implode("\n", $log);
}
}
// STARTTLS
if ($cfg['starttls']) {
smtpSend($fp, "STARTTLS");
$add("C: STARTTLS");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) {
$add("⚠️ STARTTLS not accepted. Continuing without TLS.");
} else {
$ok = @stream_socket_enable_crypto($fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT);
if (!$ok) {
$add("❌ TLS negotiation failed (OpenSSL/CA/Policy?).");
fclose($fp);
return implode("\n", $log);
}
$add("✅ TLS active.");
smtpSend($fp, "EHLO {$cfg['helo']}");
$add("C: EHLO {$cfg['helo']} (post-TLS)");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) {
$add("❌ EHLO after TLS failed.");
fclose($fp);
return implode("\n", $log);
}
}
}
// AUTH LOGIN
if ($cfg['auth']) {
if (empty($cfg['user']) || empty($cfg['pass'])) {
$add("❌ AUTH aktiviert, aber Username/Passwort fehlt.");
goto quit;
}
smtpSend($fp, "AUTH LOGIN");
$add("C: AUTH LOGIN");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['3'])) { $add("❌ AUTH LOGIN not accepted."); goto quit; }
smtpSend($fp, base64_encode($cfg['user']));
$add("C: " . base64_encode($cfg['user']) . " (user b64)");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['3'])) { $add("❌ Username step failed."); goto quit; }
smtpSend($fp, base64_encode($cfg['pass']));
$add("C: ************ (pass b64)");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) { $add("❌ Password step failed."); goto quit; }
$add("✅ AUTH successful.");
}
// Send mail
if ($cfg['sendMail']) {
if (empty($cfg['from']) || empty($cfg['to'])) {
$add("❌ SendMail aktiviert, aber From/To fehlt.");
goto quit;
}
smtpSend($fp, "MAIL FROM:<{$cfg['from']}>");
$add("C: MAIL FROM:<{$cfg['from']}>");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) { $add("❌ MAIL FROM rejected."); goto quit; }
smtpSend($fp, "RCPT TO:<{$cfg['to']}>");
$add("C: RCPT TO:<{$cfg['to']}>");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2','3'])) { $add("❌ RCPT TO rejected."); goto quit; }
smtpSend($fp, "DATA");
$add("C: DATA");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['3'])) { $add("❌ DATA not accepted."); goto quit; }
$headers = [
"From: <{$cfg['from']}>",
"To: <{$cfg['to']}>",
"Subject: {$cfg['subject']}",
"Date: " . date('r'),
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"Content-Transfer-Encoding: 8bit",
];
$msg = implode("\r\n", $headers) . "\r\n\r\n" . str_replace("\n", "\r\n", $cfg['body']) . "\r\n";
$msg = preg_replace("/\r\n\./", "\r\n..", $msg); // dot-stuffing
fwrite($fp, $msg . "\r\n.\r\n");
$add("C: [message body] + <CRLF>.<CRLF>");
$resp = smtpReadResponse($fp);
$add("S: " . implode("\nS: ", $resp));
if (!smtpExpect($resp, ['2'])) { $add("❌ Message rejected after DATA."); goto quit; }
$add("✅ Test mail accepted by server.");
} else {
$add("ℹ️ Handshake only (SendMail deaktiviert).");
}
quit:
smtpSend($fp, "QUIT");
$add("C: QUIT");
$resp = smtpReadResponse($fp);
if ($resp) $add("S: " . implode("\nS: ", $resp));
fclose($fp);
$add(str_repeat("-", 78));
$add("END TEST: {$cfg['name']}");
$add(str_repeat("-", 78));
return implode("\n", $log);
}
// -------------------- Defaults / Form handling --------------------
$defaults = [
// Relay
'relay_host' => 'YOUR-TENANT.mail.protection.outlook.com',
'relay_port' => '25',
'relay_starttls'=> '1',
'relay_auth' => '0',
'relay_user' => '',
'relay_pass' => '',
// Submission
'sub_host' => 'smtp.office365.com',
'sub_port' => '587',
'sub_starttls' => '1',
'sub_auth' => '1',
'sub_user' => 'user@YOURDOMAIN.TLD',
'sub_pass' => '',
// Common
'helo' => 'test.local',
'timeout' => '15',
'sendmail' => '1',
'from' => 'relay-test@YOURDOMAIN.TLD',
'to' => 'you@YOURDOMAIN.TLD',
'subject' => 'SMTP Test ' . date('Y-m-d H:i:s'),
'body' => "Dual SMTP test from PHP.\nTime: " . date('c') . "\n",
];
$in = $defaults;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
foreach ($in as $k => $v) {
if (isset($_POST[$k])) $in[$k] = is_string($_POST[$k]) ? trim($_POST[$k]) : $v;
}
// unchecked checkboxes are missing in POST:
foreach (['relay_starttls','relay_auth','sub_starttls','sub_auth','sendmail'] as $cb) {
$in[$cb] = isset($_POST[$cb]) ? '1' : '0';
}
}
$action = $_POST['action'] ?? '';
$results = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$common = [
'helo' => $in['helo'] ?: 'test.local',
'timeout' => (int)($in['timeout'] ?: 15),
'sendMail' => $in['sendmail'] === '1',
'from' => $in['from'],
'to' => $in['to'],
'subject' => $in['subject'],
'body' => $in['body'],
];
if ($action === 'test_relay' || $action === 'test_both') {
$cfg = array_merge($common, [
'name' => 'M365 Relay (Port 25 / MX)',
'host' => $in['relay_host'],
'port' => (int)($in['relay_port'] ?: 25),
'starttls' => $in['relay_starttls'] === '1',
'auth' => $in['relay_auth'] === '1',
'user' => $in['relay_user'],
'pass' => $in['relay_pass'],
]);
$results[] = smtpTest($cfg);
}
if ($action === 'test_sub' || $action === 'test_both') {
$cfg = array_merge($common, [
'name' => 'M365 Submission (Port 587 / smtp.office365.com)',
'host' => $in['sub_host'],
'port' => (int)($in['sub_port'] ?: 587),
'starttls' => $in['sub_starttls'] === '1',
'auth' => $in['sub_auth'] === '1',
'user' => $in['sub_user'],
'pass' => $in['sub_pass'],
]);
$results[] = smtpTest($cfg);
}
}
$serverAddr = getServerAddr();
$localIps = getLocalInterfaceIps();
$externalIp = getExternalIp();
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>SMTP Tester (Relay + Submission)</title>
<style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 18px; background:#f7f7f9; color:#111; }
.card { background:#fff; border:1px solid #e4e4e8; border-radius:14px; padding:16px; margin-bottom:14px; box-shadow: 0 1px 6px rgba(0,0,0,.04); }
.grid { display:grid; grid-template-columns: 1fr 1fr; gap:12px; }
.grid3 { display:grid; grid-template-columns: 1fr 1fr 1fr; gap:12px; }
label { display:block; font-size: 13px; color:#333; margin-bottom:6px; }
input[type="text"], input[type="password"], input[type="number"], textarea {
width:100%; padding:10px 11px; border:1px solid #d7d7dd; border-radius:10px; font-size:14px; background:#fff;
}
textarea { min-height: 110px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
.row { display:flex; gap:14px; align-items:center; flex-wrap:wrap; }
.row label { margin:0; font-size:14px; }
.btns { display:flex; gap:10px; flex-wrap:wrap; }
button {
border:0; background:#111; color:#fff; padding:10px 12px; border-radius:10px; cursor:pointer; font-size:14px;
}
button.secondary { background:#444; }
.muted { color:#555; font-size: 13px; }
pre {
background:#0b0f14; color:#d6e2ff; padding:14px; border-radius:14px; overflow:auto; white-space:pre-wrap;
border: 1px solid #1b2a3a;
}
.warn { color:#7a4b00; }
.ok { color:#0a7a2f; }
.small { font-size:12px; }
</style>
</head>
<body>
<div class="card">
<h2 style="margin:0 0 8px 0;">SMTP Tester (Microsoft Relay + Submission)</h2>
<div class="muted">
<div><b>SERVER_ADDR:</b> <?=h($serverAddr)?></div>
<div><b>Local IPv4(s):</b> <?=h(count($localIps) ? implode(", ", $localIps) : "(keine gefunden)")?></div>
<div><b>Public IPv4 (best-effort):</b> <?=h($externalIp ?: "(nicht erreichbar / geblockt)")?></div>
<div class="small warn" style="margin-top:6px;">
Tipp: Für M365-Relay per Connector ist oft genau diese Public-IP relevant (Allowlist).
</div>
</div>
</div>
<form method="post" class="card">
<h3 style="margin:0 0 10px 0;">Zielserver eingeben</h3>
<div class="grid">
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Relay (typisch Port 25 / MX)</h4>
<div class="grid3">
<div>
<label>Host</label>
<input type="text" name="relay_host" value="<?=h($in['relay_host'])?>">
</div>
<div>
<label>Port</label>
<input type="number" name="relay_port" value="<?=h($in['relay_port'])?>">
</div>
<div>
<label>Timeout (Sek.)</label>
<input type="number" name="timeout" value="<?=h($in['timeout'])?>">
</div>
</div>
<div class="row" style="margin-top:10px;">
<label><input type="checkbox" name="relay_starttls" <?=($in['relay_starttls']==='1'?'checked':'')?> > STARTTLS</label>
<label><input type="checkbox" name="relay_auth" <?=($in['relay_auth']==='1'?'checked':'')?> > AUTH LOGIN (meist aus)</label>
</div>
<div class="grid" style="margin-top:10px;">
<div>
<label>Username (falls AUTH)</label>
<input type="text" name="relay_user" value="<?=h($in['relay_user'])?>">
</div>
<div>
<label>Passwort (falls AUTH)</label>
<input type="password" name="relay_pass" value="<?=h($in['relay_pass'])?>">
</div>
</div>
<div class="muted small" style="margin-top:8px;">
Relay via Connector ist meistens <b>ohne Auth</b>. Wenn Port 25 geblockt ist, siehst du es sofort.
</div>
</div>
<div class="card" style="margin:0;">
<h4 style="margin:0 0 10px 0;">Submission (typisch Port 587 / smtp.office365.com)</h4>
<div class="grid3">
<div>
<label>Host</label>
<input type="text" name="sub_host" value="<?=h($in['sub_host'])?>">
</div>
<div>
<label>Port</label>
<input type="number" name="sub_port" value="<?=h($in['sub_port'])?>">
</div>
<div>
<label>HELO / EHLO Name</label>
<input type="text" name="helo" value="<?=h($in['helo'])?>">
</div>
</div>
<div class="row" style="margin-top:10px;">
<label><input type="checkbox" name="sub_starttls" <?=($in['sub_starttls']==='1'?'checked':'')?> > STARTTLS</label>
<label><input type="checkbox" name="sub_auth" <?=($in['sub_auth']==='1'?'checked':'')?> > AUTH LOGIN (meist an)</label>
</div>
<div class="grid" style="margin-top:10px;">
<div>
<label>Username (Mailbox)</label>
<input type="text" name="sub_user" value="<?=h($in['sub_user'])?>">
</div>
<div>
<label>Passwort / App-Passwort</label>
<input type="password" name="sub_pass" value="<?=h($in['sub_pass'])?>">
</div>
</div>
<div class="muted small" style="margin-top:8px;">
Für 587 brauchst du i.d.R. <b>Auth + STARTTLS</b>. Bei MFA/Policies ggf. App-Passwort/SMTP-Auth-Policy beachten.
</div>
</div>
</div>
<div class="card" style="margin-top:12px;">
<h4 style="margin:0 0 10px 0;">Mail / Testoptionen</h4>
<div class="row" style="margin-bottom:10px;">
<label><input type="checkbox" name="sendmail" <?=($in['sendmail']==='1'?'checked':'')?> > E-Mail wirklich senden (MAIL FROM/RCPT TO/DATA)</label>
<span class="muted">(wenn aus: nur Handshake + STARTTLS/Auth)</span>
</div>
<div class="grid">
<div>
<label>MAIL FROM</label>
<input type="text" name="from" value="<?=h($in['from'])?>">
</div>
<div>
<label>RCPT TO</label>
<input type="text" name="to" value="<?=h($in['to'])?>">
</div>
</div>
<div style="margin-top:10px;">
<label>Subject</label>
<input type="text" name="subject" value="<?=h($in['subject'])?>">
</div>
<div style="margin-top:10px;">
<label>Body</label>
<textarea name="body"><?=h($in['body'])?></textarea>
</div>
</div>
<div class="btns" style="margin-top:12px;">
<button name="action" value="test_relay" type="submit">Relay testen</button>
<button class="secondary" name="action" value="test_sub" type="submit">Submission testen</button>
<button name="action" value="test_both" type="submit">Beide testen</button>
</div>
</form>
<?php if (!empty($results)): ?>
<div class="card">
<h3 style="margin:0 0 10px 0;">Ergebnis / SMTP Dialog</h3>
<?php foreach ($results as $idx => $r): ?>
<pre><?=h($r)?></pre>
<?php endforeach; ?>
<div class="muted small">
Wenn du hier z.B. <b>Connection failed</b> siehst → meistens Firewall/Provider blockt Port 25/587.<br>
Wenn <b>MAIL FROM rejected</b> → häufig Connector/Accepted Domain/Policy.<br>
Wenn <b>TLS negotiation failed</b> → OpenSSL/CA/TLS-Policy auf dem Server.
</div>
</div>
<?php endif; ?>
</body>
</html>
Mail Relay oder Postfach Testen
