Skip to content

Land staging → master (incl. draft rendering fix) - #107

Open
asherp wants to merge 9 commits into
masterfrom
staging
Open

Land staging → master (incl. draft rendering fix)#107
asherp wants to merge 9 commits into
masterfrom
staging

Conversation

@asherp

@asherp asherp commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Lands the current staging delta onto master. The checklist below tracks each feature/fix and the feature test that guards it (or flags it as test-owed when the code has landed but no test covers it yet).

How to run

cd tauri-app/backend && cargo test --test email_integration   # what CI runs
cd tauri-app/backend && cargo test --lib

CI (.github/workflows/test.yml) runs cargo test --test email_integration on every push/PR to main, master, staging.

Legend: [x] code landed and covered by a feature test · [ ] landed but test still owed before it's safe.


🖋️ Draft rendering fix (focus)

Drafts are authored by the current user and are unsigned until sent, so hide_unsigned_messages / require_signature must never hide them (mirrors the sent-mail render path). The preview must decode both glossia clear-signed and NIP-44 encrypted bodies. Code: email-service.js renderDrafts() / renderDraftItem() (commit 5c8edce).

  • Self-authored draft renders even when hide_unsigned_messages / require_signature is on
  • NIP-44 encrypted draft body decrypts to a preview for the author
  • Glossia clear-signed draft body decodes to plaintext preview
  • Undecryptable draft only hidden when hide_undecryptable_emails is on
  • Draft attachment-count badge renders without blocking the list

Owed: no draft test exists in email_integration.rs yet — add one for the encrypted + signed preview paths.

🔏 Signing & verification — covered

  • Header-signature roundtrip (defaults_header_sig_roundtrip)
  • Full roundtrip with inline signature (defaults_full_roundtrip_with_inline_sig)
  • Tampered body invalidates signature (tampered_body_invalidates_signature)
  • Clear-signed plaintext verifies via header (clearsigned_plaintext_verifies_via_header)
  • Inline-vs-header precedence (inline_valid_header_broken_reports_body, inline_broken_header_valid_reports_header)
  • Broken pubkey fails verification (broken_pubkey_fails_verification)
  • decode_sig_and_pubkey honors schema canonical-first order

🔐 Encryption (NIP-04 / NIP-44) & replies — covered

  • NIP-04 legacy decrypt (nip04_legacy_decrypt)
  • NIP-04 X-Nostr-Sig header fallback (nip04_header_sig_fallback_unlocks_decrypt)
  • Glossia latin body roundtrip (glossia_body_latin_roundtrip)
  • Signed plaintext reply preserves nested signature (signed_plaintext_reply_preserves_nested_signature)
  • NIP-44 reply preserves nested encrypted armor (nip44_reply_preserves_nested_encrypted_armor)
  • NIP-44 three-level reply chain (nip44_three_level_reply_chain)
  • Reply threading headers + encoded quote (reply_threading_headers_and_encoded_quote)

📎 Attachments

  • Manifest attachment compose, inline-sig verifies (manifest_attachment_compose_inline_sig_verifies)
  • Manifest attachment default js-format, inline-sig verifies (manifest_attachment_default_jsformat_inline_sig_verifies)
  • Attachments render inline in thread/conversation cards
  • Inbox attachment download for self-authored mail (manual; no automated test)

📬 Sent / inbox decryption — covered

  • Sent mail decrypts via recipient header without a DM (sent_mail_decrypts_via_recipient_header_without_dm)
  • Sent mail undecryptable without any counterparty hint (sent_mail_undecryptable_without_any_counterparty_hint)
  • Multipart HTML + text (multipart_html_and_text)
  • Non-ASCII subject roundtrip (non_ascii_subject_roundtrip)
  • Quoted-printable body roundtrip (quoted_printable_body_roundtrip)

🛡️ Transport authentication (in flight — PR #103, targets staging)

  • Forged bottom-most Authentication-Results header ignored (top-most provider-stamped header selected)
  • authserv-id gate fails closed for unknown authserv-ids
  • SPF-only does not mark a message From-authenticated
  • Relaxed DMARC alignment on organizational domain (subdomain DKIM passes)

Confirm PR #103 is green and merged into staging before/with this lands.

🔄 IMAP sync, folders & spam (manual; no automated coverage yet)

  • Count-based history sync (sync_initial_count / sync_max_scan)
  • Backward UID pagination / infinite scroll on inbox + sent
  • gap_fill examines each UID once and watermarks it (no full rescan)
  • IDLE push delivers new mail on the active account
  • Spam/junk/bulk folders kept out of the main inbox
  • Spam rescue gated on transport auth, uses \Seen as intent
  • \Seen read-state syncs both directions

💬 Direct messages (manual; no automated coverage yet)

  • NIP-17 DMs deduplicated by rumor id (legacy duplicates collapse)
  • Conversation pagination with lighter contact-list previews

👥 Group / multi-recipient (spec only — future, not landing now)

Spec landed (To/Cc roles, group encryption, CONSENT block — PRs #97/#99/#100); implementation + tests are future work, tracked so they aren't lost.

  • Multi-recipient (group) encryption: one content key wrapped per recipient
  • To/Cc role semantics
  • CONSENT block disambiguates signing from replying

https://claude.ai/code/session_01G8qsd2rk22GHN3RNspKTAD


Generated by Claude Code

claude and others added 9 commits June 12, 2026 17:52
Drafts are authored by the current user and are never signed until they
are sent, so signature_valid is always null on them. renderDrafts was
filtering on signature_valid !== true whenever hide_unsigned_messages was
enabled (the default), which silently hid every saved draft from the
drafts page.

Mirror the sent-mail rendering path, which already bypasses this filter
because that mail is self-authored, and drop the now-unreachable
'no verified drafts' empty state.
Remove signature verification filter from drafts rendering
…ent workflow

Generalize the existing hybrid attachment encryption (random AES-256 key
wrapped with NIP-44) to N recipients via a per-message Content Encryption
Key and a new RECIPIENTS armor block. Define To:=signer / Cc:=viewer roles,
the sender self-stanza for Sent-folder access, per-level recipients in reply
chains, signature coverage of the recipients block, and a DocuSign-style
agreement/signing-round workflow that is independently verifiable from the
email thread.

Bumps spec to 0.3.0-draft.
spec: multi-recipient (group) encryption, To/Cc roles, and agreement workflow
Introduce a dedicated BEGIN NOSTR CONTRACT block so a signatory's consent
is an explicit, intentional act rather than a side effect of replying (every
reply is signed for chain-of-custody, so a signature alone cannot mean assent).

- New block type + END tag in the armor vocabulary (Section 2.1).
- Extend per-level signature coverage to include the CONTRACT block
  (level(L) = body || recipients || contract), fixed body->recipients->contract
  order (Section 4.2).
- Define the consent block: fields (agreement H, signer), the document hash H
  (over level-1 body+recipients, excluding contracts to avoid self-reference),
  and ordering as content (not a trailer), one per level (Section 11.3).
- Rewrite Signing Round / Completion / Verification to key off verified CONTRACT
  blocks over H, dedup by pubkey, and treat CONTRACT-less replies as comments
  (Sections 11.4-11.6); handle originator-as-signatory.
- HTML rendering for agreements: status banner + green consent panels vs muted
  comment sections, with non-colour-dependent labels (Section 6.2.6).
- Backward compatibility note: older clients ignore CONTRACT and see a plain
  signed reply (Section 12).

Bumps spec to 0.4.0-draft.
…ly privacy roadmap

- Rename the consent block CONTRACT -> CONSENT throughout (BEGIN/END NOSTR
  CONSENT, level(L) consent term, HTML rendering, completion/verification).
  CONSENT names the act, avoids colliding with 'agreement' (which SIGit and
  this spec both use for the whole container), and matches SIGit verbiage
  (signer/viewer roles are already identical). Also drop the '(Contract)'
  parenthetical from the Agreement Message heading.
- Add Section 11.7 'Relation to SIGit' mapping our model onto SIGit's
  (keys<->RECIPIENTS, meta<->H, prevSig/docSignatures<->nested signatures,
  Sign event<->CONSENT block) and noting the email-native vs relay+Blossom
  transport difference.
- Note the planned Nostr-only agreement transport (fold in SIGit's NIP-59
  gift-wrap/Blossom privacy model) in Section 10.9; emphasize the consent
  semantics are transport-independent.
…p6sr

Add consent block support for agreement workflows (v0.4.0)
verify_transport_authentication trusted the plaintext Authentication-Results
header it selected with .pop() — the bottom-most, sender-injectable header.
A sender could forge `dmarc=pass` for a spoofed From: and set
transport_verified = true.

Security fixes:
- Select the FIRST (top-most) A-R header, stamped by the closest hop (our
  receiving provider), not the last.
- Gate on authserv-id: honor an A-R header only when its organizational
  domain matches the user's provider (derived from email/IMAP host, with a
  known-provider map so Gmail's mx.google.com is trusted; fails closed
  otherwise). A sender-forged A-R with an unknown authserv-id is rejected
  even when it is the only one present.
- SPF no longer counts as From-authentication (it authenticates the envelope
  MAIL FROM, not the visible From:).

Correctness:
- DKIM alignment now uses DMARC relaxed (organizational-domain) matching, so
  legitimate subdomain signatures are not false negatives.

Threads the derived trusted-authserv set through the inbox filter and the
spam-rescue path. Adds unit tests covering the exploit, the authserv-id gate,
SPF-only, relaxed alignment, header selection, and fail-closed behavior.

Adds SECURITY.md documenting the threat model (malicious provider /
header-injection adversary), the npub-rooted trust model, the three trust
states, what transport-auth does and does not prove, and the planned
email-ownership binding.

https://claude.ai/code/session_01PyRdoum6RhwJnVyQnVrEJ3
Fix forgeable transport-auth verdict; add SECURITY.md (#101)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants