Skip to content

[DO NOT MERGE] feat: email reception with Postbox UI for identity owners#3760

Draft
aterga wants to merge 9 commits intomainfrom
arshavir/reverent-brattain
Draft

[DO NOT MERGE] feat: email reception with Postbox UI for identity owners#3760
aterga wants to merge 9 commits intomainfrom
arshavir/reverent-brattain

Conversation

@aterga
Copy link
Copy Markdown
Collaborator

@aterga aterga commented Apr 5, 2026

Summary

  • Identity owners can now receive emails at <anchor_number>@id.ai and view them in a new Postbox tab in the management dashboard.
  • An SMTP gateway accepts incoming emails, validates the recipient is a valid anchor number, and stores up to 10 emails per identity.

Changes

Backend

  • smtp_request (update) and smtp_request_validate (query) canister endpoints implementing the SMTP Gateway Protocol.
  • get_postbox (query) endpoint returning stored emails for an anchor number (newest first).
  • Persistent email storage in a new smtp_postbox stable BTreeMap (memory ID 23).
  • Validation: recipient must be <valid_u64>@id.ai, body ≤ 5 KB, ≤ 10 headers.

Frontend

  • New Postbox nav tab (with mail icon), visible only when the user has emails.
  • Two-column Postbox page: email list on the left, selected email content on the right.
  • Postbox data loaded alongside identity_info in the authenticated layout.
  • Regenerated IDL and TypeScript types from updated .did file.

🤖 Generated with Claude Code

Add `smtp_request` and `smtp_request_validate` canister endpoints
following the SMTP Gateway Protocol spec. Emails to whitelisted
users (arshavir, thomas, shiling, igor, ruediger, bjoern) at
@beta.id.ai are accepted and stored in a new `smtp_postbox` stable
BTreeMap, keeping the 10 most recent emails per recipient.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aterga aterga requested a review from a team as a code owner April 5, 2026 09:56
Copilot AI review requested due to automatic review settings April 5, 2026 09:56
@aterga aterga changed the title feat(be): implement SMTP Gateway Protocol for email reception [DO NOT MERGE] feat(be): implement SMTP Gateway Protocol for email reception Apr 5, 2026
@aterga aterga marked this pull request as draft April 5, 2026 09:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a proof-of-concept SMTP Gateway Protocol surface to the Internet Identity canister, including Candid types, request validation, and stable-memory storage of received emails.

Changes:

  • Introduces new SMTP Candid types plus smtp_request (update) and smtp_request_validate (query) canister methods.
  • Implements SMTP request validation (domain/user whitelist, size/header bounds, SMTP-style error responses).
  • Adds stable-memory “postbox” storage keyed by recipient address with per-user pruning.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/internet_identity/src/storage/storable/smtp.rs Adds stable-structures Storable* wrappers for SMTP postbox keys/values with bounded sizes.
src/internet_identity/src/storage/storable.rs Exposes the new storable::smtp module.
src/internet_identity/src/storage.rs Allocates a new stable memory region and adds store_email() with max-10 retention.
src/internet_identity/src/smtp.rs Adds canister-side handlers for storing vs. validating SMTP requests.
src/internet_identity/src/main.rs Exposes new canister endpoints under mod smtp_gateway.
src/internet_identity/internet_identity.did Adds SMTP types and service methods to the public Candid interface.
src/internet_identity_interface/src/internet_identity/types/smtp.rs Defines interface types plus validation and TryFrom to a validated internal representation.
src/internet_identity_interface/src/internet_identity/types.rs Re-exports the new smtp module.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

.find(|h| h.name.eq_ignore_ascii_case("subject"))
.map(|h| {
let mut s = h.value.clone();
s.truncate(MAX_SUBJECT_BYTES);
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

String::truncate(MAX_SUBJECT_BYTES) can panic if the subject contains multi-byte UTF-8 and byte 256 is not a char boundary, which would trap the canister on a crafted email. Truncate safely (e.g., clamp to the nearest prior is_char_boundary, or truncate by chars()), or validate/reject non-ASCII subjects before truncating by bytes.

Suggested change
s.truncate(MAX_SUBJECT_BYTES);
if s.len() > MAX_SUBJECT_BYTES {
let mut end = MAX_SUBJECT_BYTES;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
}

Copilot uses AI. Check for mistakes.
Comment on lines +199 to +212
.message
.as_ref()
.ok_or_else(|| smtp_err(SMTP_ERR_SYNTAX_ERROR, "Missing message"))?;

validate_message(message)?;

let body = String::from_utf8_lossy(&message.body).into_owned();

Ok(ValidatedSmtpRequest {
sender: format_address(&envelope.from),
recipient: format_address(&envelope.to),
subject: extract_subject(&message.headers),
body,
})
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

String::from_utf8_lossy(&message.body) can expand invalid UTF-8 into U+FFFD (3 bytes), meaning a body that passed the MAX_BODY_BYTES check can become larger than the storage bounds (and/or stored content differs from what was sent), potentially causing a trap when inserting into stable storage. Consider storing the body as bytes (Vec<u8>/ByteBuf) in ValidatedSmtpRequest/StorableEmail, or enforce that the body is valid UTF-8 and re-check/truncate the resulting string to MAX_BODY_BYTES (byte length) before storing.

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +129
fn validate_envelope(envelope: &SmtpEnvelope) -> Result<(), SmtpResponse> {
validate_address_bounds(&envelope.from, "Sender")?;
validate_address_bounds(&envelope.to, "Recipient")?;

if !envelope.to.domain.eq_ignore_ascii_case(ACCEPTED_DOMAIN) {
return Err(smtp_err(
SMTP_ERR_MAILBOX_UNAVAILABLE,
format!("Relay not permitted: domain must be {ACCEPTED_DOMAIN}"),
));
}

let user_lower = envelope.to.user.to_lowercase();
if !ACCEPTED_USERS.contains(&user_lower.as_str()) {
return Err(smtp_err(
SMTP_ERR_MAILBOX_UNAVAILABLE,
"Mailbox unavailable",
));
}

Ok(())
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

This new SMTP validation logic is security/availability sensitive (must not trap, must enforce bounds consistently). There are existing Rust unit tests in this crate (e.g., types/attributes.rs), but smtp.rs introduces many branches without tests; please add unit tests covering: missing envelope/message, non-accepted domain/user, header count/name/value limits, body size limit, and non-UTF8 body/Unicode subject cases.

Copilot uses AI. Check for mistakes.
OpenIdCredentialAddError, OpenIdCredentialRemoveError, OpenIdDelegationError,
OpenIdPrepareDelegationResponse,
};
use internet_identity_interface::internet_identity::types::smtp::{SmtpRequest, SmtpResponse};
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

The top-level use ...types::smtp::{SmtpRequest, SmtpResponse}; appears unused in this module (the SMTP types are imported again inside mod smtp_gateway). With -D warnings in CI, this likely fails compilation/clippy. Remove this import, or remove the inner import and reference the types via super::SmtpRequest/super::SmtpResponse consistently.

Suggested change
use internet_identity_interface::internet_identity::types::smtp::{SmtpRequest, SmtpResponse};

Copilot uses AI. Check for mistakes.
}

fn format_address(addr: &SmtpAddress) -> String {
format!("{}@{}", addr.user, addr.domain)
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

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

format_address preserves the original envelope.to.user/domain casing, but validation is case-insensitive (eq_ignore_ascii_case and to_lowercase for whitelisting). That means the same logical mailbox can be stored under multiple stable-map keys by varying case, bypassing the per-user pruning and enabling unbounded key growth. Canonicalize the persisted mailbox key (e.g., lowercase domain and user, since the whitelist already treats user case-insensitively) before storing.

Suggested change
format!("{}@{}", addr.user, addr.domain)
format!(
"{}@{}",
addr.user.to_ascii_lowercase(),
addr.domain.to_ascii_lowercase()
)

Copilot uses AI. Check for mistakes.
aterga and others added 3 commits April 5, 2026 14:46
- Add `get_postbox` query endpoint returning emails for an anchor number
- Add `PostboxEmail` Candid type and regenerate frontend IDL/types
- Change SMTP recipient format from hardcoded users to `anchor_number@id.ai`
- Add Postbox nav tab (visible only when emails exist) with MailIcon
- Create Postbox page with two-column layout: email list + email content
- Load postbox data in authenticated layout alongside identity info

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@aterga aterga changed the title [DO NOT MERGE] feat(be): implement SMTP Gateway Protocol for email reception [DO NOT MERGE] feat: email reception with Postbox UI for identity owners Apr 7, 2026
aterga and others added 4 commits April 7, 2026 17:11
Remove the hardcoded ACCEPTED_DOMAIN ("id.ai") restriction so emails
are accepted regardless of domain. Storage is now keyed solely by the
parsed anchor number from the recipient user part.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show the recipient address alongside From, since emails can arrive
via domain aliases that differ from the anchor number.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep main's locale translations while preserving postbox entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Real-world emails commonly exceed 10 headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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