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:
parent
8e8c9ba1dd
commit
a6740f82e3
4 changed files with 148 additions and 22 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
3
tasks.md
3
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.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.
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
||||||
|
|
|
||||||
|
|
@ -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ø)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue