Skip to content

feat(email): async per-credential mail transport (providers::mail) - #48

Merged
senamakel merged 10 commits into
tinyhumansai:mainfrom
senamakel:host-pushdown
Oct 4, 2026
Merged

senamakel merged 10 commits into
tinyhumansai:mainfrom
senamakel:host-pushdown

Conversation

@senamakel

@senamakel senamakel commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds providers::mail, an async mail transport that takes its credentials per call. It is ported from OpenCompany's server/ops/mailer.rs, smtp.rs and imap.rs so OpenCompany can delete its host-side copies. EmailChannel's async Channel::send now goes through it instead of driving the blocking SmtpTransport on a runtime worker.

New public API (tinychannels::providers::mail)

Item Feature Notes
MailCredentials::Smtp(SmtpCredentials) always Serde-tagged as provider: "smtp". Also provides provider(), from_email() and from_name().
MailProvider always Display and FromStr (case-insensitive).
SmtpSecurity always None, Starttls (the default) or Ssl.
SmtpCredentials always Fields: host, port, security, username, password, from_name, from_email.
ImapCredentials always Fields: host, port, username, password.
MailSecret always Read with expose(). Deserializes plaintext; both Debug and Serialize render "[redacted]". This mirrors OpenCompany's SecretValue guard from #1770.
OutboundEmail, InboundEmail, FetchedEmail { uid, email } always The messages that cross the seam.
MailSender::send(&creds, &email), MailReceiver::{fetch_new, mark_seen} always The two seams.
MailError always Variants: Config, InvalidAddress, Transport, TimedOut. Marked #[non_exhaustive].
LettreMailSender::{new, default, with_timeout}, DEFAULT_TIMEOUT (30s) email-send Async lettre (tokio1-rustls-tls). An empty username means no AUTH is attempted.
AsyncImapReceiver::{new, default, with_mailbox, with_timeout, mailbox}, DEFAULT_TIMEOUT (30s) email Implicit TLS with Mozilla roots and an explicit ring provider. Fetches with UID SEARCH UNSEEN + UID FETCH (UID BODY.PEEK[]); mark_seen sends UID STORE +FLAGS.SILENT (\Seen), and an empty UID list never dials.
parse_message(&[u8]) -> InboundEmail email
EmailChannel::smtp_credentials(), EmailChannel::imap_credentials() email-send smtp_tls maps to Ssl, and false maps to None.

The vocabulary links no crates, so it compiles in every build. A host can accept a dyn MailSender or write test doubles without lettre, and the heavy dependencies still follow the existing email-send / email split.

Behaviour changes

EmailChannel as a Channel (feature email) now sends through LettreMailSender:

  • It is async and bounded at 30s. The old path blocked the worker thread; a test on a current-thread runtime stalled for 60s on the old path and passes immediately on the new one.
  • An empty username now skips AUTH.
  • The blocking send_message / build_*_message surface used by send-only hosts is unchanged.

Differences from the OpenCompany port

  • Errors are typed as MailError rather than OpenCompanyError.
  • Timeouts and the IMAP mailbox are configurable instead of constants.
  • IMAP TLS uses ring (this crate's provider) instead of aws_lc_rs.
  • Not ported:
    • the test-only insecure-TLS verifier;
    • MailConfig / TenantMailboxConfig (OpenCompany-specific OPENCOMPANY_MAIL_* env parsing);
    • RecordingMailSender / RecordingMailReceiver (host test doubles).

Verification

cargo fmt --check
cargo clippy -p tinychannels --all-targets -- -D warnings                      # providers off
cargo clippy -p tinychannels --all-targets --features email-send -- -D warnings
cargo clippy -p tinychannels --all-targets --features email -- -D warnings
cargo clippy --all-targets -- -D warnings                                      # workspace
cargo test -p tinychannels                     # 800 passed (providers off)
cargo test -p tinychannels --features email-send
cargo test                                     # workspace, providers on
cargo check -p tinychannels-bus

SMTP is tested against a scripted plaintext SMTP fake: envelope, headers and body, per-call AUTH PLAIN credentials, an invalid address failing without dialing, a refused recipient, and a stalled server timing out. The IMAP protocol exchange (login, select, PEEK fetch, then store) is tested against a scripted plaintext IMAP fake through the stream-generic session functions.

The TLS connect leg of AsyncImapReceiver and the Starttls/Ssl SMTP legs are not exercised offline, because they need a certificate-bearing server. OpenCompany's live Stalwart smoke test covers that path.

Summary by CodeRabbit

  • New Features
    • Added asynchronous email sending and receiving, with configurable timeouts and support for SMTP security settings.
    • Incoming mail can be fetched without marking it as read, then acknowledged separately.
    • Added support for parsing incoming messages and mapping email channel settings to mail credentials.
  • Improvements
    • Email channel messages are now sent through the asynchronous delivery path. Subject and body selection remain unchanged.

senamakel and others added 10 commits October 3, 2026 22:44
The mail provider module was restructured to properly expose types and functions through the module hierarchy. The `types.rs` file was added to define shared data structures, and the test module was updated to import from the correct paths. This resolves compilation errors caused by missing type definitions and incorrect module references in the test suite.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add the `tokio1-rustls-tls` feature to the lettre dependency and introduce the `smtp` module behind the `email-send` feature flag. This allows the mail provider to use lettre's async SMTP transport with TLS, enabling non-blocking email sending in async contexts.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When the SMTP password is not configured, the provider now returns a clear configuration error instead of panicking with an unwrap failure. This improves user experience by providing a helpful diagnostic message during startup.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
The test module was missing explicit imports for `MailSecret` and `SmtpCredentials`, which are now brought into scope to resolve compilation errors in the smtp test suite.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
When an IMAP FETCH response lacks a UID, the parser now returns an error instead of silently proceeding with an undefined value. This prevents potential data corruption and ensures that only well-formed responses are processed.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Updated the test expectation to match the actual delivery status returned by the email provider, ensuring the test validates the correct behavior.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Add `smtp_credentials` and `imap_credentials` stub methods to `EmailChannel` for future credential retrieval. Also fix a test that incorrectly constructed a `SendMessage` by moving the `in_thread` call into the struct literal.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Reformat several long method chains and assertion calls to comply with the project's line-length convention, and replace the stub module documentation in `mod.rs` with a comprehensive description of the mail transport architecture, including the separation of concerns between outbound/inbound email and credentials, feature-gated transport implementations, and the origin of the ported code.

Auto-committed-on: macbook
Co-authored-by: Medulla <medulla@tinyhumans.ai>
Co-authored-by: Medulla <medulla@tinyhumans.ai>
@tinysweeper

tinysweeper Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Tiny Sweeper review

Tiny Sweeper reviewed this change across 6 lane(s) and found 0 active actionable finding(s). Detailed lane evidence and any incomplete work are listed below.

State: Incomplete
Priority: none
Reviewed head: 0cada97d3e08
Updated: 1791060878 (Unix time)

Review snapshot

Change surface Files Review signal Count
Production 6 Active findings 0
Tests 4 Noted findings 0
Documentation 2 Resolved findings 0
Configuration 1 Pending checks/questions 24

Completeness: Incomplete
Test assessment: No supported feature-to-test mapping was available; this does not mean tests are absent or passed.

What changed

The review could not produce a supported behavioral summary; inspect the cited changed surface and lane details below.

Features

None identified with supported citations.

Tests

No supported feature-to-test mapping was produced. Test execution is not inferred.

Findings

No active actionable findings.

Could not review: Cargo.toml, README.md, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mail/README.md, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs, src/providers/mod.rs

Before merge

  • Complete the critique review for Cargo.toml, README.md, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/README.md, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs.
  • Complete the security review for Cargo.toml, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs.

How this fits together

flowchart LR
  n0["EmailChannel<br/>changed"]:::changed
  n1["EmailConfig"]:::impacted
  n2["channel_with_allowlist"]:::impacted
  n3["Result"]:::impacted
  n4["run_idle_session"]:::impacted
  n5["fetch_unseen"]:::impacted
  n2 -->|uses| n0
  n2 -->|uses| n1
  n4 -->|uses| n3
  n5 -->|uses| n3
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading
Agent review details

critique

  • Conclusion: Neutral
  • Scope reviewed: incomplete; unanswered: Cargo.toml, README.md, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/README.md, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs
  • Lane summary: Reviewed 0 files; 0 findings. 13 files could not be reviewed: Cargo.toml, README.md, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/README.md, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs.

security

  • Conclusion: Neutral
  • Scope reviewed: incomplete; unanswered: Cargo.toml, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs
  • Lane summary: Reviewed 0 files; 0 findings. 11 files could not be reviewed: Cargo.toml, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mod.rs, src/providers/mail/mod.rs, src/providers/mail/mod_tests.rs, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/mail/types.rs. 2 files were not security-reviewed: README.md (prose or tabular data), src/providers/mail/README.md (prose or tabular data).

tests

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Introduces an async per-credential mail vocabulary, re-wires EmailChannel::send to use it, and adds a LettreMailSender and AsyncImapReceiver, each with tests that exercise the real protocol exchange against scripted TCP servers. The behavioural change is tested and cannot regress silently. _Code retrieval was unavailable (model: ladder embeddings returned 402 Payment Required: {"error":"Insufficient USD or Diem balance to complete request. Visit https://venice\.ai/settings/api to add credits."}), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

commits

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: Nothing sensitive found in what this pull request commits.

description

  • Conclusion: Success
  • Scope reviewed: all assigned evidence
  • Lane summary: Adds providers::mail: async per-credential SMTP/IMAP transport, the vocabulary traits, and LettreMailSender/AsyncImapReceiver implementations. EmailChannel::send goes through the async sender. The code looks sound and passes its tests; no issues found. _Code retrieval was unavailable (model: ladder embeddings returned 402 Payment Required: {"error":"Insufficient USD or Diem balance to complete request. Visit https://venice\.ai/settings/api to add credits."}), so this review saw the diff alone._ _Memory was unavailable (model: cortex: v1/recall: timed out after 10s), so this review ran without it._

e2e

  • Conclusion: Neutral
  • Scope reviewed: all assigned evidence
  • Lane summary: No end-to-end harness in this repository: no e2e test files and no e2e workflow.
Evidence and run details
  • Models: deepseek/deepseek-v4-flash
  • Spend: $0.001468
  • Tokens: 64983 input · 2421 output · 21760 cached · 0 embedding
Head State Pass summary
0cada97d3e08 incomplete 0 active finding(s), 0 resolved finding(s) (at 1791060878)

tinysweeper 0.1.0

@coderabbitai

coderabbitai Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 816df02a-1fbb-4156-9009-75c4cb13026e
📥 Commits

Reviewing files that changed from the base of the PR and between 4c847a4 and 0cada97.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • README.md
  • src/providers/email_channel.rs
  • src/providers/email_channel_tests.rs
  • src/providers/mail/README.md
  • src/providers/mail/imap.rs
  • src/providers/mail/imap_tests.rs
  • src/providers/mail/mod.rs
  • src/providers/mail/mod_tests.rs
  • src/providers/mail/smtp.rs
  • src/providers/mail/smtp_tests.rs
  • src/providers/mail/types.rs
  • src/providers/mod.rs

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

This change adds shared mail types and asynchronous SMTP and IMAP providers. It updates EmailChannel to send through the async SMTP provider and adds credential mapping, transport tests, and provider documentation.

Changes

Mail provider

Layer / File(s) Summary
Mail contracts and exports
src/providers/mail/types.rs, src/providers/mail/mod.rs, src/providers/mod.rs, src/providers/mail/mod_tests.rs, README.md
Adds mail credential, message, error, and sender/receiver types. Mail secrets deserialize from plaintext but are redacted in debug output and serialization. The module exports shared types unconditionally and gates SMTP and IMAP implementations by feature.
SMTP sender and EmailChannel integration
Cargo.toml, src/providers/mail/smtp.rs, src/providers/mail/smtp_tests.rs, src/providers/email_channel.rs, src/providers/email_channel_tests.rs
Adds LettreMailSender with configurable timeout and SMTP security selection. EmailChannel maps its configuration to SMTP credentials and sends an OutboundEmail through the async sender. Tests cover credential mapping, delivery, and SMTP errors.
IMAP fetching and acknowledgement
src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/README.md
Adds AsyncImapReceiver, which fetches unseen messages by UID without marking them seen. mark_seen updates supplied UIDs, and empty UID lists return without connecting. Tests cover parsing, fetching, acknowledgement, and timeout behavior.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant EmailChannel
  participant LettreMailSender
  participant SMTPServer
  EmailChannel->>LettreMailSender: Send credentials and outbound email
  LettreMailSender->>SMTPServer: Deliver message
  SMTPServer-->>LettreMailSender: Return delivery result
  LettreMailSender-->>EmailChannel: Return result or error
Loading

Merge Risk: ⚪ Minimal · up to 0cada

This adds a mail transport module and moves email sending to an async SMTP sender with a 30-second bound. No concrete merge-blocking issue was found. The TLS paths are only covered by an external live test.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 0cada

The new receiver acknowledges messages without binding them to a mailbox generation and fetches unread messages without count or byte limits. These choices can undermine recovery or allow hostile mail to exhaust a shared process. Existing SMTP security behavior is preserved; deployment exposure depends on how applications adopt the receiver.

Retained concerns

  • Medium · reliability · inferred: The new acknowledgement contract does not preserve or verify UIDVALIDITY. Fetch and acknowledgement use separate connections, discard mailbox-selection metadata, and exchange only mailbox-local UIDs. If a mailbox is recreated between operations or before recovery, an old UID can identify a different, unfiled message and mark it seen, violating the documented durable-file-before-acknowledgement invariant. UID addressing prevents sequence-number drift, but not generation reuse; downstream protection was not established.
  • Medium · security · inferred: The new batch receiver fetches complete bodies for all unseen messages and retains parsed messages in a vector without a message-count, per-message, or aggregate-byte limit. Where a host polls a mailbox that accepts attacker-supplied mail, large messages or accumulated mail can exhaust process resources and affect other accounts sharing that process. TLS and the operation timeout do not impose resource ceilings. Actual shared-process exposure and upstream mailbox limits remain unknown.
Security review details

Security Blast Radius

  • inferred — The documented design allows one transport instance to serve multiple accounts. Credential holders select external endpoints and mailbox authority; an external mail sender may control message content without possessing those credentials. Resource exhaustion in a shared host could extend beyond the attacked mailbox, although actual tenancy and process isolation were not established.

Security Findings and Attack Paths

  • inferred — The receiver’s availability attack path is mailbox-delivered content to complete-body retrieval, synchronous parsing, and accumulated output. Message count and bytes are not locally bounded, so hostile input can consume resources before the deadline provides relief. This is a newly exposed batch-receiver design risk, not evidence that the unchanged IDLE listener or an external host was exploited.

Trust Boundaries and Controls

  • observed — IMAP uses hostname-based rustls validation with Mozilla roots and safe protocol versions before login. Parsed From, subject, and body remain externally supplied strings; TLS authenticates the mailbox endpoint, not the author of each message.
  • observed — SMTP destinations and security modes come from credentials, while message addresses are validated before dialing. The adapter supports plaintext, STARTTLS, and implicit TLS and attempts AUTH only for a nonempty username. EmailChannel continues supplying configured credentials; its plaintext option predates this PR and is not an established regression.

Resilience and Maintainability Implications

  • inferred — Separate acknowledgement avoids marking messages during fetch, but safe recovery depends on preserving the original account, mailbox, and generation identity. Reconnecting with bare UIDs cannot itself reject stale acknowledgements after mailbox replacement, and a failed or cancelled STORE cannot be treated as proof that remote flags were unchanged.

Hardening Proposals

  • proposed — Return an acknowledgement identity bound to account, mailbox, UIDVALIDITY, and UID, then verify the selected mailbox generation before STORE. Define durable deduplication and reconciliation ownership so retries remain safe after partial failure or interruption.
  • proposed — Add bounded receive batches, per-message and aggregate-byte budgets, and an explicit oversized-message policy before unrestricted body retrieval. Combine these with host-level account concurrency limits so one mailbox cannot monopolize a shared mail process.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 65 functions across 10 files. (3 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: an asynchronous, per-credential mail transport in providers::mail.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 65 functions across 10 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit packed a note to send,
With subject, words, and names to lend.
The mail flew out through async air,
While unseen letters waited there.
Then carrots danced beside the den.

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

tinysweeper found nothing blocking, but could not review everything, so this is not an approval: Cargo.toml, README.md, src/providers/email_channel.rs, src/providers/email_channel_tests.rs, src/providers/mail/README.md, src/providers/mail/imap.rs, src/providers/mail/imap_tests.rs, src/providers/mail/mod.rs and 5 more.

             $0.0015 · 64,983 in / 2,421 out · 21,760 cached (33%) · deepseek/deepseek-v4-flash
tests:       $0.0006 · 21,329 in / 76 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash
description: $0.0006 · 21,847 in / 72 out    · 0 cached (0%)       · deepseek/deepseek-v4-flash

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Oct 3, 2026
@senamakel
senamakel merged commit ef347ba into tinyhumansai:main Oct 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant