feat(v1.1.7-email-inbound): webhook receiver + email:receive trigger
Inbound email: a provider POSTs a normalized JSON message to
POST /api/v1/email-inbound/{app_id}/{trigger_id}; the public receiver
verifies the optional HMAC signature, builds a TriggerEvent::Email, and
enqueues an outbox row the dispatcher delivers like any async trigger.
Handlers see ctx.event.email = #{from,to,cc,subject,text,html,
received_at,message_id}.
- migration 0024: widen triggers.kind + outbox.source_kind CHECKs to
'email'; new email_trigger_details table.
- TriggerKind::Email, TriggerDetails::Email{has_inbound_secret},
OutboxSourceKind::Email, TriggerEvent::Email; dispatcher routes the
email row via the generic resolve_trigger path.
- Admin POST /apps/{id}/triggers/email (validate_trigger_target; module
+ cross-app rejection). inbound_secret is stored ENCRYPTED via the
master key (deviation from the brief's plaintext default; decrypted
per inbound request — see HANDBACK §7).
- Dashboard: email trigger form on the Triggers tab + webhook URL +
expected-payload help.
- 8 DB-gated e2e tests (202/401/404/422/cross-app/handler-fire) +
receiver unit tests (HMAC verify, secret round-trip, payload parse).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -186,6 +186,27 @@ pub enum TriggerEvent {
|
||||
published_at: DateTime<Utc>,
|
||||
},
|
||||
|
||||
/// An inbound email (POSTed to the webhook receiver by a configured
|
||||
/// provider) fired this handler. v1.1.7. Carries the normalized
|
||||
/// message; `text`/`html` are absent when the provider sent only the
|
||||
/// other. Surfaced to scripts as `ctx.event.email`. Attachments are
|
||||
/// deferred to v1.2.
|
||||
Email {
|
||||
from: String,
|
||||
to: Vec<String>,
|
||||
#[serde(default)]
|
||||
cc: Vec<String>,
|
||||
subject: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
text: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
html: Option<String>,
|
||||
received_at: DateTime<Utc>,
|
||||
/// RFC 5322 Message-ID, when the provider supplied one.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
message_id: Option<String>,
|
||||
},
|
||||
|
||||
/// A dead-letter row fired this handler. The original event is
|
||||
/// nested verbatim plus the dead-letter metadata the design notes
|
||||
/// §4 require.
|
||||
@@ -213,6 +234,7 @@ impl TriggerEvent {
|
||||
Self::Cron { .. } => "cron",
|
||||
Self::Files { .. } => "files",
|
||||
Self::Pubsub { .. } => "pubsub",
|
||||
Self::Email { .. } => "email",
|
||||
Self::DeadLetter { .. } => "dead_letter",
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user