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:
Frank Schwenk
2026-06-11 23:19:03 +02:00
parent 04e1455d9f
commit 01de7ed4fb
16 changed files with 543 additions and 41 deletions
+6 -5
View File
@@ -1,5 +1,6 @@
# Kopie nach .env — niemals committen. # 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) --- # --- Hosting / Plesk (Classic 1.0, 0012.de) ---
PLESK_URL=https://ha01s019.org-dns.com:8443/ PLESK_URL=https://ha01s019.org-dns.com:8443/
@@ -23,15 +24,15 @@ NAMESERVER_1=ns1.org-ns.com
NAMESERVER_2=ns2.org-ns.com NAMESERVER_2=ns2.org-ns.com
# --- SMTP (Plesk-Mail auf diesem Webspace) --- # --- 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_HOST=ha01s019.org-dns.com
SMTP_PORT=587 SMTP_PORT=587
SMTP_ENCRYPTION=tls SMTP_ENCRYPTION=tls
SMTP_USER= SMTP_USER=admin@0012.de
SMTP_PASSWORD= SMTP_PASSWORD=
# Absender für System-Mails (Monitoring, Cron-Fehler) # Absender für System-Mails — Alias monitor@0012.de
SMTP_FROM=monitor@0012.de SMTP_FROM=monitor@0012.de
# Empfänger für Alarme # Empfänger für Alarme (persönliche Adresse)
ALERT_EMAIL= ALERT_EMAIL=
# --- Monitoring: Push von boka → Webspace --- # --- Monitoring: Push von boka → Webspace ---
+1
View File
@@ -1,4 +1,5 @@
.env .env
server.env
*.log *.log
/vendor/ /vendor/
/node_modules/ /node_modules/
+52 -2
View File
@@ -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. 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 ## TLS / HTTPS
Konfiguration in Plesk (Stand 2026-06-11): 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). **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 ### Geplante Nutzung
1. **Monitoring-Alerts** — z. B. `monitor@0012.de` → persönliche Adresse 1. **Monitoring-Alerts** — `monitor@0012.de` → `ALERT_EMAIL` via `sendAlert()` in `config/mail.php`
2. **SMTP für Skripte** — boka und andere Hosts senden über TLS/587 (Relay nur für autorisierte Absender konfigurieren) 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) 3. **Mail-Hosting** — Postfächer für Domains ohne eigenen Mailserver (MX auf 0012.de bzw. wint-NS)
## Geplante Aufgaben (Cron) ## Geplante Aufgaben (Cron)
+5 -4
View File
@@ -69,7 +69,8 @@ Offene Arbeiten: [`todo/README.md`](todo/README.md)
## Schnellstart (wenn eingerichtet) ## Schnellstart (wenn eingerichtet)
1. `.env.example` → `.env` kopieren und ausfüllen 1. `.env.example` → `.env` kopieren und ausfüllen (lokal, FTP)
2. `./scripts/deploy.sh` — spielt `public/` nach `httpdocs/`, Config und `.env` nach `private/` 2. `python3 scripts/sync-server-env.py` → erzeugt `server.env` für den Webspace
3. In Plesk: geplante Aufgabe für `monitor.php` (Cron-Stil, z. B. `*/10 * * * *`) 3. `./scripts/deploy.sh` — spielt `public/` + `config/` + `server.env` hoch (**.env bleibt lokal**)
4. Auf boka: Push-Skript + Crontab (siehe `todo/04-monitoring-push-boka.md`) 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
View File
@@ -3,8 +3,8 @@
declare(strict_types=1); declare(strict_types=1);
/** /**
* Load configuration from private/.env (outside document root). * Load configuration from private/server.env (outside document root).
* Called from httpdocs/*.php via require on private/config/bootstrap.php. * Local .env is never deployed — see server.env.example.
*/ */
function bootstrap(): array function bootstrap(): array
@@ -15,12 +15,29 @@ function bootstrap(): array
return $config; return $config;
} }
$envFile = dirname(__DIR__) . '/.env'; $config = loadEnvFile(envFilePath());
$config = loadEnvFile($envFile);
return $config; 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 function env(string $key, ?string $default = null): ?string
{ {
$value = bootstrap()[$key] ?? null; $value = bootstrap()[$key] ?? null;
+147
View File
@@ -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);
}
+48
View File
@@ -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
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/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 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}") 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: def upload_tree(ftp: ftplib.FTP, local_dir: Path, remote_base: str) -> None:
remote_base = remote_base.strip("/") remote_base = remote_base.strip("/")
for path in sorted(local_dir.rglob("*")): for path in sorted(local_dir.rglob("*")):
@@ -101,6 +116,13 @@ def main() -> None:
env = load_env(ROOT / ".env") env = load_env(ROOT / ".env")
require_env(env, "FTP_HOST", "FTP_USER", "FTP_PASSWORD") 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_web = env.get("FTP_REMOTE_PATH", "httpdocs").strip("/")
remote_private = env.get("FTP_PRIVATE_PATH", "private").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()) config_files = sum(1 for p in (ROOT / "config").rglob("*") if p.is_file())
print(f" public/ ({public_files} files) → {remote_web}/") print(f" public/ ({public_files} files) → {remote_web}/")
print(f" config/ ({config_files} files) → {remote_private}/config/") 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(f" (mkdir) {remote_private}/data/")
print(" .env stays local — not deployed")
if args.dry_run: if args.dry_run:
return return
@@ -122,7 +146,8 @@ def main() -> None:
upload_tree(ftp, ROOT / "public", remote_web) upload_tree(ftp, ROOT / "public", remote_web)
upload_tree(ftp, ROOT / "config", f"{remote_private}/config") upload_tree(ftp, ROOT / "config", f"{remote_private}/config")
ensure_remote_dir(ftp, f"{remote_private}/data") 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.") print("\nDone.")
finally: finally:
ftp.quit() ftp.quit()
+61
View File
@@ -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()
+39
View File
@@ -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);
}
+24
View File
@@ -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=
+1 -1
View File
@@ -29,7 +29,7 @@ Lokales Repo mit dem Webspace verbinden und eine deploybare Grundstruktur anlege
├── httpdocs/ ← public/ ├── httpdocs/ ← public/
│ └── index.php │ └── index.php
└── private/ ← nicht öffentlich └── private/ ← nicht öffentlich
├── .env ├── server.env ← Server-Secrets (deployed, nicht .env!)
├── config/bootstrap.php ├── config/bootstrap.php
└── data/ ← später last-checks.json └── data/ ← später last-checks.json
``` ```
+52 -20
View File
@@ -1,45 +1,77 @@
# 02 — E-Mail & SMTP # 02 — E-Mail & SMTP
**Priorität:** high **Priorität:** high
**Status:** erledigt (2026-06-11)
**Abhängigkeiten:** 01 **Abhängigkeiten:** 01
**Blockiert:** 03 (Alarmierung) **Blockiert:** 03 (Alarmierung)
## Ziel ## 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 ## Checkliste
### Postfächer auf 0012.de ### Postfächer auf 0012.de
- [ ] Postfach `monitor@0012.de` (oder `alerts@0012.de`) anlegen - [x] `admin@0012.de` mit Aliasen `monitor@`, `alert@`
- [ ] Passwort in `.env` → `SMTP_USER`, `SMTP_PASSWORD` - [x] Passwort in `.env` → `SMTP_USER`, `SMTP_PASSWORD`
- [ ] `ALERT_EMAIL` auf persönliche Adresse setzen - [x] `ALERT_EMAIL` gesetzt
- [ ] `SMTP_FROM=monitor@0012.de` setzen - [x] `SMTP_FROM=monitor@0012.de`
- [ ] Test: Webmail-Login auf `ha01s019.org-dns.com` - [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) - [x] `config/mail.php` — `sendMail()`, `sendAlert()`
- [ ] SPF für `0012.de` in Plesk DNS prüfen/setzen - [x] `scripts/test-mail.php` — CLI-Test
- [ ] DKIM in Plesk aktivieren (Deliverability) - [x] Port 587 + STARTTLS getestet (`php scripts/test-mail.php` → OK)
- [ ] DMARC optional: `_dmarc.0012.de` mit `p=none` zum Start - [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 - **Envelope vs. From:** SMTP-Login als `admin@`, `MAIL FROM` ebenfalls `admin@`; sichtbarer Absender bleibt `monitor@` (Alias). Plesk verwirft sonst oft still nach `250 OK`.
- [ ] In Plesk: ausgehende Mail-Limits / Relay-Regeln prüfen (Hoster-Doku) - **Test lokal:** `php scripts/test-mail.php` (liest `.env`)
- [ ] boka-Apps: zentraler SMTP-Block in Docker/env (Host, Port, User, Pass) - **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 ## Akzeptanzkriterien
- Testmail von PHP (`mail()` oder PHPMailer) an `ALERT_EMAIL` kommt an - [x] Testmail an `ALERT_EMAIL` kommt an
- SPF/DKIM ohne Hard-Fail in Mail-Header-Test (mail-tester.com o. ä.) - [ ] SPF/DKIM ohne Hard-Fail (mail-tester.com o. ä.) — optional
## Referenz ## 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
View File
@@ -17,10 +17,24 @@ Monitoring automatisch alle 5–10 Minuten ausführen.
- [ ] Ausführen: **Cron-Stil** - [ ] Ausführen: **Cron-Stil**
- [ ] Expression: `*/10 * * * *` (Pull-Checks alle 10 Min) - [ ] Expression: `*/10 * * * *` (Pull-Checks alle 10 Min)
- [ ] Beschreibung: z. B. „0012 external monitoring pull“ - [ ] 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 - [ ] **Jetzt ausführen** — Exit-Code / Log prüfen
- [ ] Optional zweite Aufgabe für leichtgewichtigen Heartbeat-Only-Check (`*/5`) — nur wenn von monitor.php getrennt - [ ] 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 ## Cron-Referenz
``` ```
+39
View File
@@ -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
View File
@@ -5,7 +5,7 @@ Nummerierte Todos für den 0012.de-Webspace. Abhängigkeiten beachten.
| # | Datei | Priorität | Kurzbeschreibung | | # | Datei | Priorität | Kurzbeschreibung |
|---|-------|-----------|------------------| |---|-------|-----------|------------------|
| 01 | [01-bootstrap-env-deploy.md](01-bootstrap-env-deploy.md) | **high** | `.env`, Repo-Struktur, erstes FTP-Deployment | | 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 | | 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` | | 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 | | 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 | | 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 | | 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 | | 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) ## Backlog (ohne Priorität)
Todo **10** — Plesk Git-Deploy und Composer; dokumentiert in [`../PLESK.md`](../PLESK.md), kein Blocker für 02–09. 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 ## Empfohlene Reihenfolge
``` ```