26.4 ferdig: Postfix som receive-only MTA med pipe til synops-mail

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.
This commit is contained in:
vegard 2026-03-19 01:32:04 +00:00
parent 8e8c9ba1dd
commit a6740f82e3
4 changed files with 148 additions and 22 deletions

View file

@ -453,7 +453,70 @@ Caddy (Docker) proxyer `api.sidelinja.org` til `host.docker.internal:3100`.
Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes Dockerfile (`maskinrommet/Dockerfile`) beholdes for referanse, men brukes
ikke i produksjon. 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 <mottaker> --sender <avsender>
→ (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) ### Lag A (minimum fungerende server)
- [ ] `https://auth.sidelinja.org` viser Authentik login - [ ] `https://auth.sidelinja.org` viser Authentik login

View file

@ -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.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 <epost> --subject <emne>` CLI-verktøy. - [x] 26.2 msmtp oppsett: konfigurer utgående epost via SMTP-relay. Avsender: `vaktmester@synops.no`. Tilgjengelig som `synops-mail --send --to <epost> --subject <emne>` CLI-verktøy.
- [x] 26.3 MX-records: sett opp MX for synops.no, sidelinja.org, vegard.info som peker til serveren. - [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`. - [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`.
> Påbegynt: 2026-03-19T01:26
- [ ] 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.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.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. - [ ] 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.

View file

@ -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-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-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-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-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-validate` | Valider at en node matcher forventet skjema for sin node_kind | Ferdig |
| `synops-backup` | PG-dump + CAS-filiste + metadata-snapshot (`--full` / `--incremental`) | Ferdig | | `synops-backup` | PG-dump + CAS-filiste + metadata-snapshot (`--full` / `--incremental`) | Ferdig |

View file

@ -1,12 +1,13 @@
// synops-mail — Send epost via msmtp. // synops-mail — Send og motta epost for Synops.
// //
// Sender epost via systemets msmtp-konfigurasjon. // Send-modus: sender epost via msmtp (Brevo SMTP-relay).
// Avsender er alltid vaktmester@synops.no. // Motta-modus: tar imot epost fra Postfix via pipe transport.
// //
// Bruk: // Bruk:
// synops-mail --send --to bruker@eksempel.no --subject "Emne" // synops-mail --send --to bruker@eksempel.no --subject "Emne"
// echo "Brødtekst" | 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" // 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. // Output: Bekreftelse til stdout ved suksess.
// Feil: stderr + exit code != 0. // Feil: stderr + exit code != 0.
@ -17,7 +18,7 @@
// //
// Ref: docs/retninger/unix_filosofi.md // Ref: docs/retninger/unix_filosofi.md
use clap::Parser; use clap::{Parser, Subcommand};
use std::io::{self, Read}; use std::io::{self, Read};
use std::process::{Command, Stdio}; 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_FROM_NAME: &str = "Synops Vaktmester";
const DEFAULT_CONFIG: &str = "/srv/synops/config/msmtp/msmtprc"; const DEFAULT_CONFIG: &str = "/srv/synops/config/msmtp/msmtprc";
/// Send epost via msmtp. /// Synops epost-verktøy: send og motta epost.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "synops-mail", about = "Send epost via msmtp")] #[command(name = "synops-mail", about = "Send og motta epost for Synops")]
struct Cli { struct Cli {
/// Send-modus (påkrevd) #[command(subcommand)]
command: Option<Commands>,
// --- Legacy flag-basert API (bakoverkompatibel) ---
/// Send-modus (legacy: bruk `send` subcommand i stedet)
#[arg(long)] #[arg(long)]
send: bool, send: bool,
/// Mottaker-epostadresse /// Motta-modus (legacy: bruk `receive` subcommand i stedet)
#[arg(long)] #[arg(long)]
to: String, receive: bool,
/// Emne /// Mottaker-epostadresse (--send)
#[arg(long)] #[arg(long)]
subject: String, to: Option<String>,
/// Brødtekst (valgfritt — leses fra stdin om ikke oppgitt) /// Emne (--send)
#[arg(long)]
subject: Option<String>,
/// Brødtekst (--send, valgfritt — leses fra stdin om ikke oppgitt)
#[arg(long)] #[arg(long)]
body: Option<String>, body: Option<String>,
/// Avsender (default: vaktmester@synops.no) /// Avsender (--send default: vaktmester@synops.no, --receive: envelope sender)
#[arg(long, default_value = DEFAULT_FROM)] #[arg(long, default_value = DEFAULT_FROM)]
from: String, from: String,
/// msmtp config-fil (default: /srv/synops/config/msmtp/msmtprc) /// msmtp config-fil (default: /srv/synops/config/msmtp/msmtprc)
#[arg(long, env = "MSMTP_CONFIG")] #[arg(long, env = "MSMTP_CONFIG")]
config: Option<String>, config: Option<String>,
/// Envelope-mottaker fra Postfix (--receive)
#[arg(long)]
recipient: Option<String>,
/// Envelope-avsender fra Postfix (--receive)
#[arg(long)]
sender: Option<String>,
}
#[derive(Subcommand)]
enum Commands {
// Subcommands kan legges til senere
} }
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
if cli.receive {
run_receive(&cli);
return;
}
if !cli.send { if !cli.send {
eprintln!("Feil: --send er påkrevd"); eprintln!("Feil: --send eller --receive er påkrevd");
std::process::exit(1); 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 // Les body fra --body eller stdin
let body = match cli.body { let body = match cli.body {
Some(b) => b, Some(b) => b,
@ -93,14 +130,14 @@ fn main() {
{body}", {body}",
from_name = DEFAULT_FROM_NAME, from_name = DEFAULT_FROM_NAME,
from = cli.from, from = cli.from,
to = cli.to, to = to,
subject = cli.subject, subject = subject,
body = body, body = body,
); );
// Send via msmtp // Send via msmtp
let mut child = Command::new("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()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
@ -125,10 +162,37 @@ fn main() {
}); });
if output.status.success() { if output.status.success() {
println!("Epost sendt til {}", cli.to); println!("Epost sendt til {}", to);
} else { } else {
let stderr = String::from_utf8_lossy(&output.stderr); let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("msmtp feilet (exit {}): {}", output.status, stderr); eprintln!("msmtp feilet (exit {}): {}", output.status, stderr);
std::process::exit(output.status.code().unwrap_or(1)); 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ø)
}