From a6740f82e34b86aa6b21985cfa33dbb46226bd9b Mon Sep 17 00:00:00 2001 From: vegard Date: Thu, 19 Mar 2026 01:32:04 +0000 Subject: [PATCH] 26.4 ferdig: Postfix som receive-only MTA med pipe til synops-mail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Postfix installert og konfigurert som lokal MTA kun for epost-mottak. Ingen relay, ingen utgående kø — utgående bruker msmtp/Brevo som før. Konfigurasjon: - virtual_mailbox_domains: synops.no, sidelinja.org, vegard.info - Catch-all: alle adresser under domenene aksepteres - virtual_transport → synops-pipe: pipe(8) leverer til synops-mail - default_transport = error: blokkerer utgående SMTP - synops-mail --receive stub: leser stdin, logger, exit 0 Verifisert: lokal SMTP-test viser at epost aksepteres, pipes til synops-mail, og logges korrekt i /var/log/mail.log. --- docs/setup/produksjon.md | 65 +++++++++++++++++++++- tasks.md | 3 +- tools/README.md | 2 +- tools/synops-mail/src/main.rs | 100 ++++++++++++++++++++++++++++------ 4 files changed, 148 insertions(+), 22 deletions(-) diff --git a/docs/setup/produksjon.md b/docs/setup/produksjon.md index eb2232c..d9c93df 100644 --- a/docs/setup/produksjon.md +++ b/docs/setup/produksjon.md @@ -453,7 +453,70 @@ Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`. Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes ikke i produksjon. -## 13. Verifisering etter oppsett +## 13. Postfix (innkommende epost) + +Postfix kjører som lokal MTA **kun for mottak**. Ingen relay, ingen +utgående kø — utgående epost sendes via msmtp/Brevo (se synops-mail --send). + +### Installasjon + +```bash +sudo DEBIAN_FRONTEND=noninteractive apt-get install -y postfix +``` + +### Konfigurasjon + +**`/etc/postfix/main.cf`** — nøkkelinnstillinger: +- `myhostname = mail.synops.no` +- `virtual_mailbox_domains = synops.no, sidelinja.org, vegard.info` +- `virtual_transport = synops-pipe:` — all epost pipes til synops-mail +- `mydestination =` — tom, all levering via virtual +- `default_transport = error:outbound mail is disabled` — blokkerer utgående +- `relayhost =` — ingen relay + +**`/etc/postfix/virtual_mailbox`** — catch-all for alle domener: +``` +@synops.no OK +@sidelinja.org OK +@vegard.info OK +``` +Etter endring: `sudo postmap /etc/postfix/virtual_mailbox` + +**`/etc/postfix/master.cf`** — pipe transport (lagt til på slutten): +``` +synops-pipe unix - n n - - pipe + flags=DRhu user=vegard argv=/usr/local/bin/synops-mail --receive --recipient ${recipient} --sender ${sender} +``` + +### Drift + +```bash +# Status +sudo postfix status + +# Restart etter konfig-endring +sudo systemctl restart postfix + +# Se epost-logg +sudo tail -f /var/log/mail.log + +# Sjekk kø (bør alltid være tom) +sudo postqueue -p +``` + +### Arkitektur + +``` +Internett → port 25 → Postfix (smtpd) + → virtual_transport = synops-pipe + → pipe: synops-mail --receive --recipient --sender + → (leser rå epost fra stdin, prosesserer, oppretter node) +``` + +Postfix eier kun transport — all forretningslogikk (brukeroppslag, +validering, node-opprettelse) ligger i synops-mail. + +## 14. Verifisering etter oppsett ### Lag A (minimum fungerende server) - [ ] `https://auth.sidelinja.org` viser Authentik login diff --git a/tasks.md b/tasks.md index a3ccecf..88f2b45 100644 --- a/tasks.md +++ b/tasks.md @@ -348,8 +348,7 @@ sidelinja.org, vegard.info) ruter til samme bruker basert på username. - [x] 26.1 Username i auth_identities: legg til `username`-kolonne, populer fra Authentik `preferred_username` ved login. Unik constraint. Oppdater auth-callback i SvelteKit til å lagre username. - [x] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to --subject ` CLI-verktøy. - [x] 26.3 MX-records: sett opp MX for synops.no, sidelinja.org, vegard.info som peker til serveren. -- [~] 26.4 Postfix minimal: installer Postfix som lokal MTA kun for mottak. Ingen relay, ingen kø for utgående. Pipe innkommende epost til `synops-mail --receive`. - > Påbegynt: 2026-03-19T01:26 +- [x] 26.4 Postfix minimal: installer Postfix som lokal MTA kun for mottak. Ingen relay, ingen kø for utgående. Pipe innkommende epost til `synops-mail --receive`. - [ ] 26.5 `synops-mail --receive`: Rust CLI som leser raw epost fra stdin. Sjekk 1: avsender-epost matcher `auth_identities.email`? Sjekk 2: innhold starter med "Kjære vaktmester" (eller konfigurerbar frase)? Begge må matche. Opprett `content`-node i brukerens innboks med epostinnholdet. Alt annet → /dev/null, ingen bounce. - [ ] 26.6 Domene-alias: `vegard@synops.no`, `vegard@sidelinja.org`, `vegard@vegard.info` ruter alle til samme bruker via username-oppslag i PG. Domenet er irrelevant. - [ ] 26.7 Utgående varsler: vaktmesteren kan sende epost-varsler til brukere (ny oppgave tildelt, innsendt artikkel godkjent, etc.) via `synops-mail --send`. Konfigurerbart per bruker i metadata.preferences. diff --git a/tools/README.md b/tools/README.md index 9731201..dbeb1f3 100644 --- a/tools/README.md +++ b/tools/README.md @@ -23,7 +23,7 @@ eller maskinrommet-API. Ligger i PATH via symlink eller direkte kall. | `synops-node` | Hent/vis en node med edges (UUID, --depth, --format json/md) | Ferdig | | `synops-ai` | LLM-verktøy: `prompt` (direkte LLM-kall) + `script` (orkestreringsscript fra fritekst) | Ferdig | | `synops-clip` | Hent og parse webartikler (Readability + Playwright-fallback, paywall-deteksjon) | Ferdig | -| `synops-mail` | Send epost via msmtp (vaktmester@synops.no) | Ferdig (venter SMTP-credentials) | +| `synops-mail` | Send epost via msmtp, motta via Postfix pipe (`--send` / `--receive`) | Ferdig | | `synops-notify` | Send varsel via epost, WebSocket-push, eller begge | Ferdig | | `synops-validate` | Valider at en node matcher forventet skjema for sin node_kind | Ferdig | | `synops-backup` | PG-dump + CAS-filiste + metadata-snapshot (`--full` / `--incremental`) | Ferdig | diff --git a/tools/synops-mail/src/main.rs b/tools/synops-mail/src/main.rs index 9a009d1..d73fb81 100644 --- a/tools/synops-mail/src/main.rs +++ b/tools/synops-mail/src/main.rs @@ -1,12 +1,13 @@ -// synops-mail — Send epost via msmtp. +// synops-mail — Send og motta epost for Synops. // -// Sender epost via systemets msmtp-konfigurasjon. -// Avsender er alltid vaktmester@synops.no. +// Send-modus: sender epost via msmtp (Brevo SMTP-relay). +// Motta-modus: tar imot epost fra Postfix via pipe transport. // // Bruk: // synops-mail --send --to bruker@eksempel.no --subject "Emne" // echo "Brødtekst" | synops-mail --send --to bruker@eksempel.no --subject "Emne" // synops-mail --send --to bruker@eksempel.no --subject "Emne" --body "Tekst her" +// cat raw_email.eml | synops-mail --receive --recipient bruker@synops.no --sender avsender@test.no // // Output: Bekreftelse til stdout ved suksess. // Feil: stderr + exit code != 0. @@ -17,7 +18,7 @@ // // Ref: docs/retninger/unix_filosofi.md -use clap::Parser; +use clap::{Parser, Subcommand}; use std::io::{self, Read}; use std::process::{Command, Stdio}; @@ -25,43 +26,79 @@ const DEFAULT_FROM: &str = "vaktmester@synops.no"; const DEFAULT_FROM_NAME: &str = "Synops Vaktmester"; const DEFAULT_CONFIG: &str = "/srv/synops/config/msmtp/msmtprc"; -/// Send epost via msmtp. +/// Synops epost-verktøy: send og motta epost. #[derive(Parser)] -#[command(name = "synops-mail", about = "Send epost via msmtp")] +#[command(name = "synops-mail", about = "Send og motta epost for Synops")] struct Cli { - /// Send-modus (påkrevd) + #[command(subcommand)] + command: Option, + + // --- Legacy flag-basert API (bakoverkompatibel) --- + + /// Send-modus (legacy: bruk `send` subcommand i stedet) #[arg(long)] send: bool, - /// Mottaker-epostadresse + /// Motta-modus (legacy: bruk `receive` subcommand i stedet) #[arg(long)] - to: String, + receive: bool, - /// Emne + /// Mottaker-epostadresse (--send) #[arg(long)] - subject: String, + to: Option, - /// Brødtekst (valgfritt — leses fra stdin om ikke oppgitt) + /// Emne (--send) + #[arg(long)] + subject: Option, + + /// Brødtekst (--send, valgfritt — leses fra stdin om ikke oppgitt) #[arg(long)] body: Option, - /// Avsender (default: vaktmester@synops.no) + /// Avsender (--send default: vaktmester@synops.no, --receive: envelope sender) #[arg(long, default_value = DEFAULT_FROM)] from: String, /// msmtp config-fil (default: /srv/synops/config/msmtp/msmtprc) #[arg(long, env = "MSMTP_CONFIG")] config: Option, + + /// Envelope-mottaker fra Postfix (--receive) + #[arg(long)] + recipient: Option, + + /// Envelope-avsender fra Postfix (--receive) + #[arg(long)] + sender: Option, +} + +#[derive(Subcommand)] +enum Commands { + // Subcommands kan legges til senere } fn main() { let cli = Cli::parse(); + if cli.receive { + run_receive(&cli); + return; + } + if !cli.send { - eprintln!("Feil: --send er påkrevd"); + eprintln!("Feil: --send eller --receive er påkrevd"); std::process::exit(1); } + let to = cli.to.as_deref().unwrap_or_else(|| { + eprintln!("Feil: --to er påkrevd for --send"); + std::process::exit(1); + }); + let subject = cli.subject.as_deref().unwrap_or_else(|| { + eprintln!("Feil: --subject er påkrevd for --send"); + std::process::exit(1); + }); + // Les body fra --body eller stdin let body = match cli.body { Some(b) => b, @@ -93,14 +130,14 @@ fn main() { {body}", from_name = DEFAULT_FROM_NAME, from = cli.from, - to = cli.to, - subject = cli.subject, + to = to, + subject = subject, body = body, ); // Send via msmtp let mut child = Command::new("msmtp") - .args(["--file", &config_path, "--from", &cli.from, &cli.to]) + .args(["--file", &config_path, "--from", &cli.from, to]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -125,10 +162,37 @@ fn main() { }); if output.status.success() { - println!("Epost sendt til {}", cli.to); + println!("Epost sendt til {}", to); } else { let stderr = String::from_utf8_lossy(&output.stderr); eprintln!("msmtp feilet (exit {}): {}", output.status, stderr); std::process::exit(output.status.code().unwrap_or(1)); } } + +/// Motta epost fra Postfix pipe transport. +/// +/// Leser rå RFC 5322-epost fra stdin og logger mottaket. +/// Faktisk prosessering (brukeroppslag, node-opprettelse) implementeres i oppgave 26.5. +fn run_receive(cli: &Cli) { + let recipient = cli.recipient.as_deref().unwrap_or("(ukjent)"); + let sender = cli.sender.as_deref().unwrap_or("(ukjent)"); + + // Les rå epost fra stdin + let mut raw_email = String::new(); + if let Err(e) = io::stdin().read_to_string(&mut raw_email) { + eprintln!("synops-mail receive: feil ved lesing fra stdin: {e}"); + // Exit 0 slik at Postfix ikke prøver på nytt + std::process::exit(0); + } + + let size = raw_email.len(); + + // Logg mottaket (syslog via stderr som Postfix fanger) + eprintln!( + "synops-mail receive: mottatt epost fra={sender} til={recipient} størrelse={size} bytes" + ); + + // Stub: aksepter og logg. Oppgave 26.5 implementerer brukeroppslag og node-opprettelse. + // Exit 0 = levert OK (Postfix sletter fra kø) +}