<?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

Beitragsnavigation