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:
+6
-5
@@ -1,5 +1,6 @@
|
||||
# Kopie nach .env — niemals committen.
|
||||
# Alle Werte aus der wint.global Willkommens-Mail bzw. Plesk.
|
||||
# Nur für diese Maschine: Deploy (FTP), lokale Tests.
|
||||
# Server-Secrets → server.env (siehe server.env.example), nicht .env deployen!
|
||||
|
||||
# --- Hosting / Plesk (Classic 1.0, 0012.de) ---
|
||||
PLESK_URL=https://ha01s019.org-dns.com:8443/
|
||||
@@ -23,15 +24,15 @@ NAMESERVER_1=ns1.org-ns.com
|
||||
NAMESERVER_2=ns2.org-ns.com
|
||||
|
||||
# --- SMTP (Plesk-Mail auf diesem Webspace) ---
|
||||
# Für Monitoring-Alerts und Skripte, die über 0012.de mailen
|
||||
# Login: Hauptpostfach admin@0012.de (Aliase: monitor@, alert@)
|
||||
SMTP_HOST=ha01s019.org-dns.com
|
||||
SMTP_PORT=587
|
||||
SMTP_ENCRYPTION=tls
|
||||
SMTP_USER=
|
||||
SMTP_USER=admin@0012.de
|
||||
SMTP_PASSWORD=
|
||||
# Absender für System-Mails (Monitoring, Cron-Fehler)
|
||||
# Absender für System-Mails — Alias monitor@0012.de
|
||||
SMTP_FROM=monitor@0012.de
|
||||
# Empfänger für Alarme
|
||||
# Empfänger für Alarme (persönliche Adresse)
|
||||
ALERT_EMAIL=
|
||||
|
||||
# --- Monitoring: Push von boka → Webspace ---
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.env
|
||||
server.env
|
||||
*.log
|
||||
/vendor/
|
||||
/node_modules/
|
||||
|
||||
+52
-2
@@ -42,6 +42,34 @@ DNS-Einträge nur für Domains mit Bezug zum Paket oder über wint.global bestel
|
||||
|
||||
Domain-Delegation kann nach Registration bis zu **24 Stunden** dauern. Bis dahin ggf. temporäre URL aus dem Kundenbereich nutzen.
|
||||
|
||||
### DNS-Zugang (Classic 1.0)
|
||||
|
||||
Bei wint.global liegt die **DNS-Zone oft beim Hoster** (`ns1.org-ns.com`), nicht beim Kunden in Plesk. SPF und DMARC sind gesetzt; **DKIM-TXT-Records fehlen öffentlich**, obwohl DKIM in Plesk aktiviert ist — typisch, wenn der Hoster DNS pflegt und die Mail-Records nicht synchronisiert hat.
|
||||
|
||||
**Du musst DNS nicht selbst pflegen.** Optionen:
|
||||
|
||||
1. **Support-Ticket** an support@wint.global — DKIM-DNS für `0012.de` hinterlegen (siehe unten)
|
||||
2. **Workaround:** `ALERT_EMAIL` auf Gmail o. ä. setzen, solange Tuta/strenge Empfänger blockieren
|
||||
|
||||
#### Support-Ticket (DKIM)
|
||||
|
||||
In Plesk (ohne DNS-Rechte geht das trotzdem):
|
||||
|
||||
**Mail → Mail-Einstellungen → „Externe DNS konfigurieren“** — zwei TXT-Records kopieren und an den Support schicken, z. B.:
|
||||
|
||||
> Bitte folgende TXT-Records für 0012.de in der DNS-Zone ergänzen (DKIM ist in Plesk aktiv, Records fehlen in DNS):
|
||||
>
|
||||
> - `default._domainkey.0012.de` → *(Wert aus Plesk)*
|
||||
> - `_domainkey.0012.de` → *(Wert aus Plesk)*
|
||||
|
||||
Prüfung danach:
|
||||
|
||||
```bash
|
||||
dig +short TXT default._domainkey.0012.de @ns1.org-ns.com
|
||||
```
|
||||
|
||||
Kundennummer **26064** angeben.
|
||||
|
||||
## TLS / HTTPS
|
||||
|
||||
Konfiguration in Plesk (Stand 2026-06-11):
|
||||
@@ -73,10 +101,32 @@ curl -sI https://0012.de/ # → 200, HSTS-Header
|
||||
|
||||
**Sicherheit:** Nach 5 fehlgeschlagenen Login-Versuchen wird die Client-IP für 10 Minuten gesperrt (FTP, Webmail, E-Mail).
|
||||
|
||||
### Postfächer 0012.de (Stand 2026-06-11)
|
||||
|
||||
| Adresse | Rolle |
|
||||
|---------|--------|
|
||||
| `admin@0012.de` | Hauptpostfach, **SMTP-Login** für Skripte |
|
||||
| `monitor@0012.de` | Alias — Absender Monitoring (`SMTP_FROM`) |
|
||||
| `alert@0012.de` | Alias — optional alternativer Absender |
|
||||
|
||||
Webmail / SMTP-Auth: immer die **Hauptadresse** + Postfach-Passwort, nicht den Alias-Namen allein.
|
||||
|
||||
### Authentifizierung (SPF / DKIM / DMARC)
|
||||
|
||||
| | Status |
|
||||
|---|--------|
|
||||
| **SPF** | ✅ gesetzt (Hoster) — nichts tun |
|
||||
| **DKIM** | ⚠️ in Plesk an, **DNS-Record fehlt** — Support-Ticket (kein DNS-Zugang) |
|
||||
| **DMARC** | ✅ `v=DMARC1;p=none;` (Hoster) — nichts tun |
|
||||
|
||||
DMARC ist optional zum Start (`p=none` = nur Reports, keine Ablehnung). Erst nach SPF/DKIM-Stabilisierung auf `quarantine`/`reject` erhöhen. Optional `rua=mailto:admin@0012.de` für Aggregate-Reports ergänzen.
|
||||
|
||||
Prüfung: [mail-tester.com](https://www.mail-tester.com) oder Test mit `php scripts/test-mail.php`.
|
||||
|
||||
### Geplante Nutzung
|
||||
|
||||
1. **Monitoring-Alerts** — z. B. `monitor@0012.de` → persönliche Adresse
|
||||
2. **SMTP für Skripte** — boka und andere Hosts senden über TLS/587 (Relay nur für autorisierte Absender konfigurieren)
|
||||
1. **Monitoring-Alerts** — `monitor@0012.de` → `ALERT_EMAIL` via `sendAlert()` in `config/mail.php`
|
||||
2. **SMTP von boka** — siehe [`todo/11-boka-smtp-relay.md`](todo/11-boka-smtp-relay.md)
|
||||
3. **Mail-Hosting** — Postfächer für Domains ohne eigenen Mailserver (MX auf 0012.de bzw. wint-NS)
|
||||
|
||||
## Geplante Aufgaben (Cron)
|
||||
|
||||
@@ -69,7 +69,8 @@ Offene Arbeiten: [`todo/README.md`](todo/README.md)
|
||||
|
||||
## Schnellstart (wenn eingerichtet)
|
||||
|
||||
1. `.env.example` → `.env` kopieren und ausfüllen
|
||||
2. `./scripts/deploy.sh` — spielt `public/` nach `httpdocs/`, Config und `.env` nach `private/`
|
||||
3. In Plesk: geplante Aufgabe für `monitor.php` (Cron-Stil, z. B. `*/10 * * * *`)
|
||||
4. Auf boka: Push-Skript + Crontab (siehe `todo/04-monitoring-push-boka.md`)
|
||||
1. `.env.example` → `.env` kopieren und ausfüllen (lokal, FTP)
|
||||
2. `python3 scripts/sync-server-env.py` → erzeugt `server.env` für den Webspace
|
||||
3. `./scripts/deploy.sh` — spielt `public/` + `config/` + `server.env` hoch (**.env bleibt lokal**)
|
||||
4. In Plesk: geplante Aufgabe für `monitor.php` (Cron-Stil, z. B. `*/10 * * * *`)
|
||||
5. Auf boka: Push-Skript + Crontab (siehe `todo/04-monitoring-push-boka.md`)
|
||||
|
||||
+21
-4
@@ -3,8 +3,8 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Load configuration from private/.env (outside document root).
|
||||
* Called from httpdocs/*.php via require on private/config/bootstrap.php.
|
||||
* Load configuration from private/server.env (outside document root).
|
||||
* Local .env is never deployed — see server.env.example.
|
||||
*/
|
||||
|
||||
function bootstrap(): array
|
||||
@@ -15,12 +15,29 @@ function bootstrap(): array
|
||||
return $config;
|
||||
}
|
||||
|
||||
$envFile = dirname(__DIR__) . '/.env';
|
||||
$config = loadEnvFile($envFile);
|
||||
$config = loadEnvFile(envFilePath());
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
function envFilePath(): string
|
||||
{
|
||||
$baseDir = dirname(__DIR__);
|
||||
|
||||
// Webspace: private/config/bootstrap.php → private/server.env
|
||||
if (basename($baseDir) === 'private') {
|
||||
return $baseDir . '/server.env';
|
||||
}
|
||||
|
||||
// Local repo: .env on this machine (never deployed)
|
||||
$local = $baseDir . '/.env';
|
||||
if (is_readable($local)) {
|
||||
return $local;
|
||||
}
|
||||
|
||||
return $baseDir . '/server.env';
|
||||
}
|
||||
|
||||
function env(string $key, ?string $default = null): ?string
|
||||
{
|
||||
$value = bootstrap()[$key] ?? null;
|
||||
|
||||
+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);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* One-off SMTP test from the webspace (same path as monitor.php cron).
|
||||
* GET /mail-test.php?token=MONITOR_INGEST_TOKEN
|
||||
* Remove or restrict after verification.
|
||||
*/
|
||||
|
||||
require_once dirname(__DIR__) . '/private/config/mail.php';
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$token = $_GET['token'] ?? '';
|
||||
$expected = env('MONITOR_INGEST_TOKEN') ?? '';
|
||||
|
||||
if ($expected === '' || !hash_equals($expected, $token)) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => false, 'error' => 'forbidden']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$to = env('ALERT_EMAIL');
|
||||
if (!$to) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => 'ALERT_EMAIL not set']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$domain = env('DOMAIN', '0012.de');
|
||||
$subject = "[{$domain}] SMTP test (server) " . gmdate('Y-m-d H:i:s') . ' UTC';
|
||||
$body = implode("\n", [
|
||||
'0012.de SMTP test — sent from webspace PHP (cron path)',
|
||||
'',
|
||||
'From header: ' . env('SMTP_FROM'),
|
||||
'SMTP auth: ' . env('SMTP_USER'),
|
||||
'',
|
||||
'If you receive this, server-side mail works.',
|
||||
]);
|
||||
|
||||
try {
|
||||
sendMail($to, $subject, $body);
|
||||
echo json_encode(['ok' => true, 'to' => $to, 'from' => env('SMTP_FROM')]);
|
||||
} catch (Throwable $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['ok' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
+28
-3
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deploy public/, config/, and .env to wint.global / Plesk via FTP/FTPS."""
|
||||
"""Deploy public/, config/, and server.env to wint.global / Plesk via FTP/FTPS."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -85,6 +85,21 @@ def upload_file(ftp: ftplib.FTP, local: Path, remote_path: str) -> None:
|
||||
print(f" ↑ {local.relative_to(ROOT)} → {remote_path}")
|
||||
|
||||
|
||||
def delete_remote_file(ftp: ftplib.FTP, remote_path: str) -> None:
|
||||
remote_path = remote_path.replace("\\", "/").lstrip("/")
|
||||
remote_dir = os.path.dirname(remote_path)
|
||||
remote_name = os.path.basename(remote_path)
|
||||
try:
|
||||
if remote_dir:
|
||||
ensure_remote_dir(ftp, remote_dir)
|
||||
else:
|
||||
cwd_to_root(ftp)
|
||||
ftp.delete(remote_name)
|
||||
print(f" ✕ deleted {remote_path}")
|
||||
except ftplib.error_perm as exc:
|
||||
print(f" (skip delete {remote_path}: {exc})")
|
||||
|
||||
|
||||
def upload_tree(ftp: ftplib.FTP, local_dir: Path, remote_base: str) -> None:
|
||||
remote_base = remote_base.strip("/")
|
||||
for path in sorted(local_dir.rglob("*")):
|
||||
@@ -101,6 +116,13 @@ def main() -> None:
|
||||
env = load_env(ROOT / ".env")
|
||||
require_env(env, "FTP_HOST", "FTP_USER", "FTP_PASSWORD")
|
||||
|
||||
server_env = ROOT / "server.env"
|
||||
if not server_env.is_file():
|
||||
sys.exit(
|
||||
"Missing server.env — run: python3 scripts/sync-server-env.py\n"
|
||||
"(or copy server.env.example to server.env and fill in server values)"
|
||||
)
|
||||
|
||||
remote_web = env.get("FTP_REMOTE_PATH", "httpdocs").strip("/")
|
||||
remote_private = env.get("FTP_PRIVATE_PATH", "private").strip("/")
|
||||
|
||||
@@ -109,8 +131,10 @@ def main() -> None:
|
||||
config_files = sum(1 for p in (ROOT / "config").rglob("*") if p.is_file())
|
||||
print(f" public/ ({public_files} files) → {remote_web}/")
|
||||
print(f" config/ ({config_files} files) → {remote_private}/config/")
|
||||
print(f" .env → {remote_private}/.env")
|
||||
print(f" server.env → {remote_private}/server.env")
|
||||
print(f" (remove legacy {remote_private}/.env if present)")
|
||||
print(f" (mkdir) {remote_private}/data/")
|
||||
print(" .env stays local — not deployed")
|
||||
|
||||
if args.dry_run:
|
||||
return
|
||||
@@ -122,7 +146,8 @@ def main() -> None:
|
||||
upload_tree(ftp, ROOT / "public", remote_web)
|
||||
upload_tree(ftp, ROOT / "config", f"{remote_private}/config")
|
||||
ensure_remote_dir(ftp, f"{remote_private}/data")
|
||||
upload_file(ftp, ROOT / ".env", f"{remote_private}/.env")
|
||||
upload_file(ftp, server_env, f"{remote_private}/server.env")
|
||||
delete_remote_file(ftp, f"{remote_private}/.env")
|
||||
print("\nDone.")
|
||||
finally:
|
||||
ftp.quit()
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Copy server-relevant keys from local .env to server.env (never deploys .env)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
SERVER_KEYS = [
|
||||
"DOMAIN",
|
||||
"SMTP_HOST",
|
||||
"SMTP_PORT",
|
||||
"SMTP_ENCRYPTION",
|
||||
"SMTP_USER",
|
||||
"SMTP_PASSWORD",
|
||||
"SMTP_FROM",
|
||||
"ALERT_EMAIL",
|
||||
"MONITOR_INGEST_TOKEN",
|
||||
"MONITOR_INGEST_URL",
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"TELEGRAM_CHAT_ID",
|
||||
]
|
||||
|
||||
|
||||
def load_env(path: Path) -> dict[str, str]:
|
||||
env: dict[str, str] = {}
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
key, _, value = line.partition("=")
|
||||
env[key.strip()] = value.strip()
|
||||
return env
|
||||
|
||||
|
||||
def main() -> None:
|
||||
local = ROOT / ".env"
|
||||
target = ROOT / "server.env"
|
||||
|
||||
if not local.is_file():
|
||||
raise SystemExit(f"Missing {local}")
|
||||
|
||||
src = load_env(local)
|
||||
lines = [
|
||||
"# Generated by scripts/sync-server-env.py — do not commit",
|
||||
"# Deploy: ./scripts/deploy.sh uploads this to private/server.env",
|
||||
"",
|
||||
]
|
||||
|
||||
for key in SERVER_KEYS:
|
||||
if key in src:
|
||||
lines.append(f"{key}={src[key]}")
|
||||
|
||||
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
alert = src.get("ALERT_EMAIL", "?")
|
||||
print(f"Wrote {target.name} (ALERT_EMAIL={alert})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Send a test mail using local .env (this machine only).
|
||||
* Usage: php scripts/test-mail.php
|
||||
*/
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require_once "{$root}/config/mail.php";
|
||||
|
||||
$to = env('ALERT_EMAIL');
|
||||
if (!$to) {
|
||||
fwrite(STDERR, "ALERT_EMAIL not set in .env\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$from = env('SMTP_FROM', env('SMTP_USER'));
|
||||
$domain = env('DOMAIN', '0012.de');
|
||||
|
||||
$subject = "[{$domain}] SMTP test (local) " . gmdate('Y-m-d H:i:s') . ' UTC';
|
||||
$body = implode("\n", [
|
||||
'0012.de SMTP test — sent from local machine',
|
||||
'',
|
||||
'From header: ' . $from,
|
||||
'SMTP auth: ' . env('SMTP_USER'),
|
||||
'',
|
||||
'If you receive this, local SMTP settings work.',
|
||||
]);
|
||||
|
||||
try {
|
||||
sendMail($to, $subject, $body);
|
||||
echo "Sent test mail to {$to} (From: {$from})\n";
|
||||
} catch (Throwable $e) {
|
||||
fwrite(STDERR, 'Failed: ' . $e->getMessage() . "\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Kopie nach server.env — niemals committen.
|
||||
# Wird nach private/server.env auf dem Webspace deployed.
|
||||
# Nur Werte, die PHP-Skripte auf 0012.de brauchen (kein FTP/Plesk).
|
||||
|
||||
DOMAIN=0012.de
|
||||
|
||||
# SMTP (0012.de Postfach)
|
||||
SMTP_HOST=ha01s019.org-dns.com
|
||||
SMTP_PORT=587
|
||||
SMTP_ENCRYPTION=tls
|
||||
SMTP_USER=admin@0012.de
|
||||
SMTP_PASSWORD=
|
||||
SMTP_FROM=monitor@0012.de
|
||||
|
||||
# Alarm-Empfänger
|
||||
ALERT_EMAIL=
|
||||
|
||||
# Monitoring
|
||||
MONITOR_INGEST_TOKEN=
|
||||
MONITOR_INGEST_URL=https://0012.de/ingest.php
|
||||
|
||||
# Optional
|
||||
TELEGRAM_BOT_TOKEN=
|
||||
TELEGRAM_CHAT_ID=
|
||||
@@ -29,7 +29,7 @@ Lokales Repo mit dem Webspace verbinden und eine deploybare Grundstruktur anlege
|
||||
├── httpdocs/ ← public/
|
||||
│ └── index.php
|
||||
└── private/ ← nicht öffentlich
|
||||
├── .env
|
||||
├── server.env ← Server-Secrets (deployed, nicht .env!)
|
||||
├── config/bootstrap.php
|
||||
└── data/ ← später last-checks.json
|
||||
```
|
||||
|
||||
+52
-20
@@ -1,45 +1,77 @@
|
||||
# 02 — E-Mail & SMTP
|
||||
|
||||
**Priorität:** high
|
||||
**Status:** erledigt (2026-06-11)
|
||||
**Abhängigkeiten:** 01
|
||||
**Blockiert:** 03 (Alarmierung)
|
||||
|
||||
## Ziel
|
||||
|
||||
Mail-Infrastruktur auf 0012.de für Monitoring-Alerts, Skript-Versand und spätere Fremddomains.
|
||||
Mail-Infrastruktur auf 0012.de für Monitoring-Alerts und Skript-Versand.
|
||||
|
||||
Relay von **boka** → ausgelagert nach [`11-boka-smtp-relay.md`](11-boka-smtp-relay.md).
|
||||
|
||||
## Postfach-Setup (Plesk)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Postfach | `admin@0012.de` |
|
||||
| Aliase | `monitor@0012.de`, `alert@0012.de` |
|
||||
| SMTP-Login | `admin@0012.de` (Hauptpostfach, nicht Alias) |
|
||||
| Absender Monitoring | `SMTP_FROM=monitor@0012.de` |
|
||||
| Alarm-Empfänger | `ALERT_EMAIL` → persönliche Adresse |
|
||||
|
||||
## Checkliste
|
||||
|
||||
### Postfächer auf 0012.de
|
||||
|
||||
- [ ] Postfach `monitor@0012.de` (oder `alerts@0012.de`) anlegen
|
||||
- [ ] Passwort in `.env` → `SMTP_USER`, `SMTP_PASSWORD`
|
||||
- [ ] `ALERT_EMAIL` auf persönliche Adresse setzen
|
||||
- [ ] `SMTP_FROM=monitor@0012.de` setzen
|
||||
- [ ] Test: Webmail-Login auf `ha01s019.org-dns.com`
|
||||
- [x] `admin@0012.de` mit Aliasen `monitor@`, `alert@`
|
||||
- [x] Passwort in `.env` → `SMTP_USER`, `SMTP_PASSWORD`
|
||||
- [x] `ALERT_EMAIL` gesetzt
|
||||
- [x] `SMTP_FROM=monitor@0012.de`
|
||||
- [x] Webmail-Login auf `ha01s019.org-dns.com`
|
||||
|
||||
### SMTP für Skripte
|
||||
### SMTP für Skripte (0012.de)
|
||||
|
||||
- [ ] Port 587 + STARTTLS testen (von lokalem Rechner / boka)
|
||||
- [ ] SPF für `0012.de` in Plesk DNS prüfen/setzen
|
||||
- [ ] DKIM in Plesk aktivieren (Deliverability)
|
||||
- [ ] DMARC optional: `_dmarc.0012.de` mit `p=none` zum Start
|
||||
- [x] `config/mail.php` — `sendMail()`, `sendAlert()`
|
||||
- [x] `scripts/test-mail.php` — CLI-Test
|
||||
- [x] Port 587 + STARTTLS getestet (`php scripts/test-mail.php` → OK)
|
||||
- [x] SPF gesetzt (Hoster-DNS)
|
||||
- [x] DKIM in Plesk aktiviert
|
||||
- [ ] DKIM in **öffentlicher DNS** — fehlt; Hoster per Ticket (kein DNS-Zugang im Paket)
|
||||
- [x] DMARC vorhanden (Hoster-DNS)
|
||||
|
||||
### Relay von boka (später)
|
||||
### Technische Notizen
|
||||
|
||||
- [ ] Entscheiden: dediziertes Relay-Postfach `relay@0012.de` vs. App-spezifische Absender
|
||||
- [ ] In Plesk: ausgehende Mail-Limits / Relay-Regeln prüfen (Hoster-Doku)
|
||||
- [ ] boka-Apps: zentraler SMTP-Block in Docker/env (Host, Port, User, Pass)
|
||||
- **Envelope vs. From:** SMTP-Login als `admin@`, `MAIL FROM` ebenfalls `admin@`; sichtbarer Absender bleibt `monitor@` (Alias). Plesk verwirft sonst oft still nach `250 OK`.
|
||||
- **Test lokal:** `php scripts/test-mail.php` (liest `.env`)
|
||||
- **Test vom Webspace:** `mail-test.php?token=…` (liest `private/server.env`)
|
||||
- **Tuta/strenge Empfänger:** bis DKIM-DNS steht → `ALERT_EMAIL` Gmail o. ä.
|
||||
- **Kein DNS-Zugang:** DKIM-Records aus Plesk Mail-Einstellungen an wint-Support — siehe [`../HOSTING.md`](../HOSTING.md#dns-zugang-classic-10)
|
||||
|
||||
### Cron-Benachrichtigungen Plesk
|
||||
## `.env` (Beispiel)
|
||||
|
||||
- [ ] Geplante Aufgaben: „Nur Fehler“ für PHP-Fatal-Errors — ergänzt App-eigene Alerts, ersetzt sie nicht
|
||||
```env
|
||||
SMTP_USER=admin@0012.de
|
||||
SMTP_FROM=monitor@0012.de
|
||||
ALERT_EMAIL=you@example.com
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
php scripts/test-mail.php
|
||||
```
|
||||
|
||||
Nach Deploy: `server.env` auf Server, Test lokal mit `.env`.
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- Testmail von PHP (`mail()` oder PHPMailer) an `ALERT_EMAIL` kommt an
|
||||
- SPF/DKIM ohne Hard-Fail in Mail-Header-Test (mail-tester.com o. ä.)
|
||||
- [x] Testmail an `ALERT_EMAIL` kommt an
|
||||
- [ ] SPF/DKIM ohne Hard-Fail (mail-tester.com o. ä.) — optional
|
||||
|
||||
## Referenz
|
||||
|
||||
- [`../HOSTING.md`](../HOSTING.md) — SMTP-Host `ha01s019.org-dns.com`
|
||||
- [`../HOSTING.md`](../HOSTING.md)
|
||||
- [`../config/mail.php`](../config/mail.php)
|
||||
- boka-Relay: [`11-boka-smtp-relay.md`](11-boka-smtp-relay.md)
|
||||
|
||||
+15
-1
@@ -17,10 +17,24 @@ Monitoring automatisch alle 5–10 Minuten ausführen.
|
||||
- [ ] Ausführen: **Cron-Stil**
|
||||
- [ ] Expression: `*/10 * * * *` (Pull-Checks alle 10 Min)
|
||||
- [ ] Beschreibung: z. B. „0012 external monitoring pull“
|
||||
- [ ] Benachrichtigen: **Nur Fehler** (PHP-Crash → Plesk-Mail)
|
||||
- [ ] Benachrichtigen: **Nur Fehler** (siehe unten)
|
||||
- [ ] **Jetzt ausführen** — Exit-Code / Log prüfen
|
||||
- [ ] Optional zweite Aufgabe für leichtgewichtigen Heartbeat-Only-Check (`*/5`) — nur wenn von monitor.php getrennt
|
||||
|
||||
## Benachrichtigen: „Nur Fehler“
|
||||
|
||||
Erst beim Anlegen der geplanten Aufgabe in Plesk relevant (zusammen mit `monitor.php`).
|
||||
|
||||
| Option | Bedeutung |
|
||||
|--------|-----------|
|
||||
| **Nicht benachrichtigen** | Cron läuft still; Crash fällt nicht auf |
|
||||
| **Nur Fehler** | Plesk mailt nur bei Skript-Fehler (PHP-Fatal, Exit ≠ 0, stderr) |
|
||||
| **Jedes Mal** | Mail bei jeder Ausführung — bei `*/10` unbrauchbar |
|
||||
|
||||
**Empfehlung: „Nur Fehler“** — meldet, wenn `monitor.php` auf 0012.de selbst kaputt ist.
|
||||
|
||||
**Getrennt von Monitoring-Alerts:** Ausfall von `schwenk.online` kommt aus `sendAlert()` → `ALERT_EMAIL`. Plesk-Cron-Mail betrifft nur die Cron-Ausführung auf dem Webspace.
|
||||
|
||||
## Cron-Referenz
|
||||
|
||||
```
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 11 — SMTP-Relay von boka
|
||||
|
||||
**Priorität:** medium
|
||||
**Status:** offen
|
||||
**Abhängigkeiten:** [02-email-smtp.md](02-email-smtp.md) (erledigt)
|
||||
**Blockiert:** boka-Apps, die über 0012.de mailen sollen
|
||||
|
||||
## Ziel
|
||||
|
||||
Prozesse auf **boka** (Docker-Stacks, Cron-Skripte) senden ausgehende Mail über den Plesk-SMTP von `0012.de`.
|
||||
|
||||
## Voraussetzungen (0012.de — erledigt in Todo 02)
|
||||
|
||||
- Postfach `admin@0012.de` mit Aliasen `monitor@`, `alert@`
|
||||
- SMTP TLS/587 auf `ha01s019.org-dns.com`
|
||||
- DKIM aktiv
|
||||
|
||||
## Checkliste
|
||||
|
||||
- [ ] Entscheiden: dediziertes Relay-Postfach vs. `admin@` + alias From-Adressen
|
||||
- [ ] In Plesk: ausgehende Mail-Limits / Relay-Regeln prüfen (Hoster-Doku)
|
||||
- [ ] SPF für `0012.de` verifizieren (boka-IPs nur wenn direkt sendend — bei Relay nicht nötig)
|
||||
- [ ] boka: zentraler SMTP-Block (Docker env / `.env` auf boka)
|
||||
- `SMTP_HOST=ha01s019.org-dns.com`
|
||||
- `SMTP_PORT=587`
|
||||
- `SMTP_USER=admin@0012.de`
|
||||
- `SMTP_PASSWORD=…` (gleiches Postfach oder App-Passwort)
|
||||
- [ ] Test von boka: `curl`/swaks oder kleines Script → Mail an persönliche Adresse
|
||||
- [ ] Pro Stack: From-Adresse festlegen (`monitor@0012.de`, `alert@0012.de`, …)
|
||||
|
||||
## Akzeptanzkriterien
|
||||
|
||||
- Mindestens ein Dienst auf boka sendet erfolgreich über 0012.de-SMTP
|
||||
- Keine Mails im Spam (DKIM pass)
|
||||
|
||||
## Referenz
|
||||
|
||||
- [`../HOSTING.md`](../HOSTING.md) — SMTP-Zugang
|
||||
- [`../config/mail.php`](../config/mail.php) — Referenz-Implementierung (PHP)
|
||||
+4
-1
@@ -5,7 +5,7 @@ Nummerierte Todos für den 0012.de-Webspace. Abhängigkeiten beachten.
|
||||
| # | Datei | Priorität | Kurzbeschreibung |
|
||||
|---|-------|-----------|------------------|
|
||||
| 01 | [01-bootstrap-env-deploy.md](01-bootstrap-env-deploy.md) | **high** | `.env`, Repo-Struktur, erstes FTP-Deployment |
|
||||
| 02 | [02-email-smtp.md](02-email-smtp.md) | **high** | Postfächer, SMTP für Alerts und Relay |
|
||||
| 02 | [02-email-smtp.md](02-email-smtp.md) | **high** | Postfächer, SMTP für Alerts (0012.de) |
|
||||
| 03 | [03-monitoring-pull-phase1.md](03-monitoring-pull-phase1.md) | **high** | `monitor.php`, HTTPS/SSL-Checks, Alarm |
|
||||
| 04 | [04-monitoring-push-boka.md](04-monitoring-push-boka.md) | **medium** | Push von boka → `ingest.php` |
|
||||
| 05 | [05-plesk-cron.md](05-plesk-cron.md) | **high** | Geplante Aufgaben in Plesk |
|
||||
@@ -14,11 +14,14 @@ Nummerierte Todos für den 0012.de-Webspace. Abhängigkeiten beachten.
|
||||
| 08 | [08-monitoring-phase2.md](08-monitoring-phase2.md) | **medium** | API-Health, Latenz, Backup-Heartbeat |
|
||||
| 09 | [09-monitoring-phase3.md](09-monitoring-phase3.md) | **low** | Rest-Domains, Dashboard, Header-Checks |
|
||||
| 10 | [10-optional-plesk-git-composer.md](10-optional-plesk-git-composer.md) | backlog | Plesk Git / Composer evaluieren |
|
||||
| 11 | [11-boka-smtp-relay.md](11-boka-smtp-relay.md) | **medium** | boka-Apps über 0012.de-SMTP |
|
||||
|
||||
## Backlog (ohne Priorität)
|
||||
|
||||
Todo **10** — Plesk Git-Deploy und Composer; dokumentiert in [`../PLESK.md`](../PLESK.md), kein Blocker für 02–09.
|
||||
|
||||
Todo **11** — boka SMTP-Relay; abhängig von erledigtem **02**, kein Blocker für **03**.
|
||||
|
||||
## Empfohlene Reihenfolge
|
||||
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user