TL;DR: For an education platform that sends a login code before presenting a required compliance notice, start with one managed transactional delivery path behind a small typed adapter. SMTP relay is acceptable when its integration and event evidence meet the same contract. Add a second provider only after you can distinguish accepted, delivered, delayed, bounced, and unknown outcomes without issuing duplicate codes. The durable asset is the evidence record, not the transport.
| Pick | Use it when | Integration effort | Evidence boundary | Main trap |
|---|---|---|---|---|
| One managed path | The team needs the smallest operable system | Low | One normalized event stream plus application logs | Treating provider acceptance as delivery |
| Two managed paths | An independent failure path is required and tested | Medium to high | Two event models mapped into one state machine | Retrying through both paths and sending two valid codes |
| Self-managed SMTP | Mail operations are already a staffed capability | High | Queue, SMTP replies, DSNs, and mailbox-provider signals | Owning a mail transfer system by accident |
This is the decision rule: choose the least complex option that can produce the audit evidence your compliance workflow actually requires. For many teams, that means one path first.
Fast is good.
Explainable is better.
The code and the notice are separate records. A code proves possession of a channel for a short-lived authentication step; the notice record proves which notice version the application presented after authentication and when the user acknowledged it. An email delivery event cannot prove that a person read or accepted legal text. Keep that distinction explicit in the data model.
Should OTP email use an SMTP relay or mixed providers?
SMTP, specified by RFC 5321, defines a store-and-forward transfer protocol. A successful reply means the receiving SMTP system accepted responsibility for the message at that hop. It does not, by itself, establish inbox placement, human reading, or acknowledgement of a compliance notice. RFC 5322 defines Internet Message Format and separates message structure from the transfer mechanism. Delivery Status Notifications in RFC 3461 add a standardized notification format, but their use is an SMTP extension and a sender must still handle partial or absent evidence. Mailbox-provider guidance adds another operational layer: authenticate mail, publish the expected DNS records, use valid forward and reverse DNS, and monitor complaint behavior. Those are delivery-system concerns, not authentication-domain concerns. For an OTP, time changes the meaning of every event. A delivery signal received after the code expires may be true and still be useless to the login attempt. A bounce can justify offering a fresh attempt through an allowed recovery path; it must never cause the old code to be replayed. A compact diagram in words helps: login attempt creates challenge; challenge creates one delivery command; the adapter submits it; provider events update delivery evidence; successful code verification closes the challenge; authenticated application flow presents the compliance notice; acknowledgement stores the notice version and actor. Each arrow has its own identifier.
Unknown stays unknown.
Do not silently rewrite it as delivered.
Pick one managed path when integration speed matters
A single managed path is the clean baseline when the engineering team does not already operate mail infrastructure. The adapter can use an HTTPS API or an SMTP submission interface. The protocol choice matters less than the contract around it: idempotent submission, bounded timeouts, redacted logs, correlation identifiers, and events that can be reconciled later.
This option has a crisp observability story. Emit one counter for submission outcomes, one histogram for submission latency, and one counter for normalized delivery-state transitions. Alert on ratios over a meaningful window, not on each individual bounce. Put the challenge ID, message ID, notice version, and provider key in structured fields, but never put the OTP itself in logs, traces, metrics, or error messages.
The trade-off is concentration. A provider-side incident, a local integration error, or a sender-reputation problem can affect the whole path. A second integration does not automatically fix the last category: two providers sending from the same poorly managed domain can inherit the same authentication and reputation mistakes.
That distinction matters.
Pick two paths only with a deterministic failover policy
A mixed-provider setup earns its complexity when the organization has a stated recovery objective, can operate two independently configured paths, and rehearses failover. It is not a reliability checkbox. You now own two credential lifecycles, two event schemas, two suppression models, two sets of rate behavior, and the logic that decides when uncertainty is safe to retry.
Set the rule before deploying. For example, fail over only when the primary submission definitively fails before acceptance. If the submission times out after the request leaves your process, classify the result as unknown and reconcile it before sending another live code. Otherwise, a slow success on path A plus a retry on path B creates two messages and a confusing login race.
The safest recovery usually creates a new challenge with a new code, invalidates the prior challenge, and records the reason. That is more visible to the application, but it preserves a single-current-code invariant. The UI should expose a resend action only after a controlled delay and should rate-limit attempts by account and other risk signals chosen by the security team. Exact limits belong in the threat model; inventing universal numbers would be false precision.
Test mixed delivery as a state machine. Exercise definitive rejection, timeout-before-response, delayed event, duplicate event, out-of-order event, hard bounce, expired challenge, and successful verification followed by late delivery evidence. The interesting failures live between systems.
Test the late event.
Implement the evidence boundary once
The following TypeScript sketch keeps transport responses away from the authentication domain. It also makes a critical split: delivery evidence belongs to a message attempt, while code validity belongs to a challenge.
type DeliveryState =
| "queued"
| "accepted"
| "delivered"
| "delayed"
| "bounced"
| "rejected"
| "unknown";
type SendCommand = {
attemptId: string;
challengeId: string;
recipient: string;
templateId: string;
templateData: { code: string; expiresAt: string };
};
type Submission = {
attemptId: string;
transportMessageId: string;
state: "accepted" | "rejected" | "unknown";
observedAt: string;
};
interface DeliveryAdapter {
send(command: SendCommand, idempotencyKey: string): Promise<Submission>;
}
The idempotency key should identify the message attempt, not just the user. Reusing a user ID would collapse legitimate later challenges. Generate the code in the authentication service, store only what your verification design requires, and pass the plaintext code only to the rendering boundary that needs it. Keep secrets out of telemetry.
Inbound events need the same discipline. Authenticate the event using the mechanism defined by the selected transport, retain the original event for forensic review according to your retention policy, and apply a normalized transition exactly once. Event IDs provide deduplication; occurrence time preserves ordering evidence. Arrival time alone is insufficient because queues delay events.
type DeliveryEvent = {
eventId: string;
attemptId: string;
transportMessageId: string;
state: DeliveryState;
occurredAt: string;
receivedAt: string;
};
type AuditEntry = {
subjectId: string;
action: "notice_presented" | "notice_acknowledged";
noticeVersion: string;
occurredAt: string;
sessionId: string;
};
async function recordDeliveryEvent(event: DeliveryEvent): Promise<void> {
if (await eventStore.has(event.eventId)) return;
await eventStore.transaction(async (tx) => {
await tx.appendOriginal(event);
await tx.applyMonotonicTransition(event.attemptId, event.state, event.occurredAt);
});
}
applyMonotonicTransition is domain logic, not a lexical sort. Delivered should not become accepted because an older event arrived late. A later bounce may still be meaningful under the transport's event semantics, so document allowed transitions and test them against real fixtures from every path.
For the compliance notice, write a separate append-only entry when the authenticated application presents the exact version, then another when the user takes the defined acknowledgement action. Link those entries to the session and subject. Do not copy sensitive email payloads into the audit table merely because storage is available. Retention, access control, and deletion rules still apply.
Operations become much easier when dashboards follow this model. Graph challenge creation, submission state, terminal delivery evidence, verification success, expiry, notice presentation, and acknowledgement as separate stages. A gap between accepted and delivered points toward mail delivery. A gap between delivered and verified can indicate expiry, user behavior, or mailbox delay; the metric cannot decide which one without more evidence. A gap after verification belongs to the application notice flow.
Pick self-managed SMTP only when mail operations are intentional
Self-managed SMTP can be appropriate for an organization that already staffs mail delivery, needs direct control of queue behavior, and accepts responsibility for DNS, authentication, reputation, bounce processing, abuse handling, and operational coverage. It is rarely the lowest-integration-effort answer for a product team building its first login-code flow.
The attraction is control. The cost is an expanded system boundary. Queue age, connection failures, retry schedules, DSN parsing, authentication alignment, complaint signals, and key rotation all become your operational work. The application still needs the same adapter and evidence model, so this choice does not remove domain-level engineering.
Limits to keep visible
Email OTP is channel possession evidence, not proof of identity and not proof that a compliance notice was read. NIST's digital identity guidance states that email must not be used for out-of-band authentication; teams working under that assurance model need a different authenticator. Even outside that model, security review should decide whether email codes fit the threat level.
SMTP relay is not inherently wrong, and an HTTP submission API is not inherently reliable. Either can sit behind the boundary described here. The practical limit is evidence: if a path cannot support authenticated submission, traceable attempts, normalized outcomes, suppression handling, and rehearsed recovery without ambiguous duplicate codes, it is not ready for this workflow.
Build the smallest observable path. Add redundancy only when its failure policy is clearer than the failure it is meant to solve.
Top comments (0)