1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
//! Per-user outbound-send policy — the sender-identity rule for any paired
//! outbound email.
//!
//! Sending stays per-user (a banked design input): a paired outbound email — a
//! sequence step, a follow-up, an auto-acknowledgement — goes *from the connected
//! user's verified address*, never from a shared or default mailbox. This module
//! encodes that rule as a pure, fail-loud policy: [`resolve_sender_identity`]
//! reads the connected user's verified sending address out of the host
//! [`auth_context`](crate::HostContext::auth_context) and refuses, loudly, when
//! there is none.
//!
//! The identity is host-owned by construction — it comes from the authenticated
//! context the server populates, not from guest input — so a function cannot
//! choose to send from another address. That is the enforcement: the only sending
//! identity a function can obtain is its own connected user's, and the absence of
//! one is an error rather than a silent fall-back to a shared mailbox.
//!
//! A first-class `send_email` host op that injects the bound `from` and a concrete
//! SMTP / provider transport are a planned hardening follow-up on this policy (see
//! `docs/architecture/native-runtime-ergonomics.md`); this module ships the
//! enforceable rule that op will call, and the reference workload
//! `examples/native-functions/follow-up-email.ts` mirrors it in `TypeScript`.
use ;
use Value;
/// An owned, `Send` boxed future — the object-safe async return used to keep
/// [`SenderIdentityResolver`] dyn-dispatchable without adding a new dyn-dispatch
/// trait-macro (the workspace ratchet).
pub type BoxFuture<'a, T> = ;
/// The connected user's verified sending identity — the only `from` a paired
/// outbound email may use.
/// A refusal to send: the per-user policy could not be satisfied.
///
/// The policy fails loud rather than fabricating a sender, so a misconfiguration
/// can never cause a send from the wrong — or a shared — mailbox.
/// Resolve the connected user's verified sending identity from a host auth
/// context.
///
/// The address is taken from the `email` field the host populates from the
/// authenticated identity. It must be a non-empty, plausibly-addressable string
/// (containing an `@`); anything else — a missing, blank, or malformed value —
/// is a refusal, because a paired outbound email must be sent from the connected
/// user's address and never from a shared or default mailbox.
///
/// # Errors
///
/// Returns [`SendPolicyError`] when the auth context carries no usable verified
/// sending address.
/// The injectable seam the `send_email` host op calls to obtain a host-owned
/// `from` (DESIGN §4.2).
///
/// One implementation per deployment, object-safe so the server can inject an
/// `Arc<dyn SenderIdentityResolver>` into the functions host.
/// The default [`LoginEmailSender`] is the degenerate case — the sending address
/// *is* the connected user's login email, read from the auth context with no DB.
/// A DB-backed implementation (in the server) resolves `sub → verified
/// from-address + mailbox` on the shared identity primitive, cached and
/// fail-closed. Either way a refusal is a [`SendPolicyError`], never a silent
/// fall-back to a shared mailbox.
/// The degenerate [`SenderIdentityResolver`]: the sending address is the
/// connected user's login email, read from the host auth context (no DB).
///
/// This subsumes the pure [`resolve_sender_identity`] policy as a trait
/// implementation (DESIGN §4.1) — the seam works with no `[identity.sender]`
/// configured, and a DB-backed resolver replaces it verbatim where the sending
/// mailbox differs from the login email.
;
/// A guest's request to send an email via the `send_email` host op.
///
/// The `from` is **not** part of this request: it is host-owned and injected by
/// the op from the resolved [`SenderIdentity`], so a guest can never send from
/// another address. A `from` field in the guest's JSON is silently ignored (it
/// maps to no field here), which is the enforcement — the guest cannot override
/// the sender.
/// The result of a successful [`send_email`](crate::HostContext::send_email).
/// The per-dispatch context a [`send`](EmailTransport::send) carries beyond the
/// sender and the request: the correlation send-id and the tenant scope.
///
/// The `send_id` is the host's per-dispatch idempotency token (see
/// [`idempotency_token`](crate::HostContext::idempotency_token)); the transport
/// uses it two ways — as the VERP `bounces+<send-id>@…` Return-Path that
/// correlates an inbound bounce/challenge/reply back to this send, and as an
/// exactly-once key so a durable retry of an already-sent dispatch does not
/// double-send. The `tenant` scopes the send-status and suppression rows the
/// transport reads/writes. Both are `Option` because a zero-config deployment (no
/// HMAC secret, no tenant) sends without correlation or tenant scoping.
///
/// A named struct rather than positional `Option<&str>` parameters: `send_id` and
/// `tenant` are both optional strings and would be trivially transposable at the
/// call site.
/// The transport seam the `send_email` host op relays through, once the op has
/// resolved the host-owned `from` from the [`SenderIdentityResolver`].
///
/// Object-safe so the server can inject an `Arc<dyn EmailTransport>` into the
/// functions host; the concrete per-connected-account SMTP transport lives in
/// `fraiseql-server` (the runtime-SMTP owner), mirroring the resolver split.
///
/// The returned [`FraiseQLError`](fraiseql_error::FraiseQLError) carries the status
/// that durable dispatch classifies by: a **4xx** (e.g. `Validation`/403) is a
/// permanent failure routed straight to the dead-letter queue, a **5xx** (e.g.
/// `ServiceUnavailable`) is transient and retried.