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
//! Email channel — `wasm-smtp` 0.9 + `wasm-smtp-tokio` adapter.
//!
//! ## Design (Phase 1, peisear 0.16.0)
//!
//! - Plain text only. HTML / multipart is Phase 2 if real
//! demand appears; the 80% case for notification email is
//! one paragraph of UTF-8.
//! - One-shot connection per send. No pooling. Notification
//! volume is small; pooling complexity isn't earned yet.
//! - Both implicit TLS (port 465) and STARTTLS (port 587) are
//! supported via [`super::config::TlsMode`]. Both flavours
//! are first-class in the `wasm-smtp` family — implicit TLS
//! uses [`TokioTlsTransport::connect_implicit_tls`],
//! STARTTLS uses [`TokioPlainTransport::connect`] +
//! [`SmtpClient::connect_starttls`].
//! - Composition uses [`MessageBuilder`] passed straight to
//! [`SmtpClient::send_message`]. The `mail-builder` cargo
//! feature on `wasm-smtp-tokio` enables this convenience.
//! - Authentication uses [`SmtpClient::login`], which
//! auto-selects the strongest mechanism the server
//! advertised: SCRAM-SHA-256 > PLAIN > LOGIN as of
//! `wasm-smtp` 0.9.0.
//!
//! ## Why so little code lives here
//!
//! Per Q3 of the 0.16.0 design ("project management is the
//! business, not email"), the SMTP-related surface area in
//! peisear is deliberately tiny. The wasm-smtp family takes
//! care of:
//!
//! - Transport plumbing (TCP, TLS handshake, SNI, root certs)
//! → `wasm-smtp-tokio`.
//! - SMTP state machine, parsing, command formatting,
//! dot-stuffing, error classification → `wasm-smtp`.
//! - RFC 5322 / MIME composition (line folding, encoded-word,
//! header injection defenses) → `mail-builder`.
//! - SCRAM-SHA-256 / PLAIN / LOGIN authentication → `wasm-smtp`.
//!
//! What's left for us is: mapping our config to the right
//! transport, calling four methods, and wrapping the error.
use MessageBuilder;
use SmtpClient;
use ;
use ;
/// Email-channel failure. Wraps `wasm-smtp`'s top-level error
/// so caller-side error formatters (anyhow's `{:#}`, eyre,
/// manual `.source()` walks) see the full diagnostic chain
/// (e.g. `EmailError → SmtpError::Io → io::Error`).
// `MessageBuilder::write_to_string` may surface std::io::Error;
// we don't currently call that directly (we hand the builder
// to `send_message`), but if a future refactor switches to the
// manual path the From impl below will be useful.
/// EHLO identifier we send. RFC 5321 §4.1.1.1 expects an FQDN;
/// most submission servers treat it as advisory (they verify
/// the actual connection, not this string). A stable
/// identifier across deployments is fine.
const EHLO_IDENTIFIER: &str = "peisear.local";
/// Send one notification email.
///
/// `to` is a single recipient — notifications are per-user, so
/// no batching. `subject` is plain UTF-8 (RFC 2047
/// encoded-word is handled by `mail-builder` for non-ASCII).
/// `body` is plain UTF-8 with `\n` line endings; `mail-builder`
/// CRLF-normalizes for the wire.
///
/// Returns `Ok(())` on a 250-acknowledged delivery. Errors:
/// transport setup, TLS handshake, AUTH, SMTP envelope
/// rejection, post-DATA `.` rejection — all are surfaced as
/// [`EmailError::Smtp`] with the wasm-smtp variant preserved.
///
/// Retry policy is the caller's concern. We don't retry here.
pub async