Skip to content

Report per-recipient email delivery status; detect bounced/undeliverable notifications #188

Description

@rubenhensen

Problem

When PostGuard sends a file, cryptify emails each recipient a download link — send_email() in src/email.rs, called from upload_finalize (POST /fileupload/finalize/<uuid>). Today that function returns a single Ok("Email successfully sent") as soon as the SMTP relay accepts each message, and the finalize endpoint reports success. The website (postguard-website) then shows the sender a blanket "sent successfully" confirmation.

This is misleading in two ways:

  1. SMTP acceptance ≠ delivery. mailer.send(&email) returning Ok only means the relay accepted the message for onward delivery (a 250 at submission). A syntactically-valid-but-wrong address — e.g. a mistyped jane@gmial.com — is accepted at submission and bounces asynchronously: the receiving MTA rejects it later and a DSN/bounce is returned to the Return-Path, which cryptify never reads. The sender is told "sent" when in fact nobody received the file. This is exactly the silent failure observed in the July 2026 user test — see Mistyped recipient email fails silently — no error and no email is sent postguard-website#293.

  2. No per-recipient status. The recipient loop in send_email() uses ?, so the first synchronous submission error aborts the whole batch and collapses to one opaque error. There is no structured, per-recipient result the website could use to say "email to x@x.com failed."

Desired behaviour

Report delivery status per recipient so the website can tell the sender exactly which addresses succeeded and which failed, instead of a blanket "sent". There are two timescales:

A. Synchronous submission failures (quick win)

  • Don't abort the batch on the first failure — attempt every recipient and collect a per-recipient result, e.g. { email, status: sent | failed, error }.
  • Return that per-recipient status from send_email() and surface it in the upload_finalize response, so the website's send confirmation can render "delivered to A, failed for B" rather than always "sent successfully".

B. Asynchronous bounces (the harder, real part)

True bounces arrive after the HTTP response is already gone, so they can't be waited on synchronously. Detecting them needs one of:

  • a monitored Return-Path / bounce mailbox + DSN parsing, correlated to a send via VERP or a stored Message-ID; or
  • moving notification mail to an ESP with a bounce webhook/API (SES / Postmark / SendGrid / Mailgun).

When a bounce is detected, notify the original sender so they can act — this is the backend half of encryption4all/postguard-website#276 ("the original sender should be informed of a bounced/not delivered email"). Optionally, a pre-send MX / mailbox check would catch many bad domains before submission.

Relevant code

  • src/email.rssend_email() (recipient loop + Ok("Email successfully sent"))
  • src/main.rsupload_finalize, POST /fileupload/finalize/<uuid> (awaits send_email, returns the finalize responder)
  • Notification mail uses the lettre SMTP crate (raw relay, no bounce handling today).

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions