wasm_smtp_component/
lib.rs1#[cfg(target_arch = "wasm32")]
34mod bindings {
35 wit_bindgen::generate!({
36 path: "../../wit",
37 world: "smtp-client",
38 exports: {
41 "wasm-smtp:smtp/smtp-send": super::SmtpSendImpl,
42 },
43 });
44}
45
46#[cfg(target_arch = "wasm32")]
49use bindings::exports::wasm_smtp::smtp::smtp_send::{
50 Guest, SendError, SendResult, SmtpConfig, SmtpCredentials, SmtpMessage, TlsMode,
51};
52
53#[cfg(not(target_arch = "wasm32"))]
55mod stubs;
56
57#[cfg(not(target_arch = "wasm32"))]
58use stubs::{SendError, SendResult, SmtpConfig, SmtpCredentials, SmtpMessage};
59
60pub struct SmtpSendImpl;
64
65impl SmtpSendImpl {
66 #[allow(unused_variables)]
68 pub fn send_impl(
69 config: SmtpConfig,
70 credentials: SmtpCredentials,
71 message: SmtpMessage,
72 ) -> Result<SendResult, SendError> {
73 use wasm_smtp::SmtpError;
74
75 #[cfg(target_arch = "wasm32")]
77 let client_result = {
78 let opts = ConnectOptions::default();
79 let fut = match config.tls_mode {
80 TlsMode::Implicit => wasm_smtp_wasi::connect_smtps(
81 &config.host,
82 config.port,
83 &config.ehlo_domain,
84 ),
85 TlsMode::Starttls => wasm_smtp_wasi::connect_smtp_starttls(
86 &config.host,
87 config.port,
88 &config.ehlo_domain,
89 ),
90 };
91 wasm_smtp_component_rt::block_on(fut)
94 };
95
96 #[cfg(not(target_arch = "wasm32"))]
97 let client_result: Result<_, SmtpError> =
98 Err(SmtpError::Io(wasm_smtp::IoError::new(
99 "wasm-smtp-component only runs on wasm32-wasip2; \
100 use cargo test for native unit tests",
101 )));
102
103 let mut _client = client_result.map_err(smtp_error_to_wit)?;
104
105 #[cfg(target_arch = "wasm32")]
107 wasm_smtp_component_rt::block_on(
108 client.login(&credentials.username, &credentials.password),
109 )
110 .map_err(smtp_error_to_wit)?;
111
112 let to_refs: Vec<&str> = message.to.iter().map(String::as_str).collect();
114
115 #[cfg(target_arch = "wasm32")]
116 let outcome = wasm_smtp_component_rt::block_on(client.send_mail(
117 &message.from,
118 &to_refs,
119 &message.raw_message,
120 ))
121 .map_err(smtp_error_to_wit)?;
122
123 #[cfg(not(target_arch = "wasm32"))]
124 #[allow(unreachable_code)]
125 let _outcome: wasm_smtp::SendOutcome = {
126 let _ = (&message.from, &to_refs, &message.raw_message);
127 return Err(SendError::Io("unreachable on native host".into()));
128 unreachable!()
129 };
130
131 #[cfg(target_arch = "wasm32")]
133 { wasm_smtp_component_rt::block_on(client.quit()).ok(); }
134
135 #[cfg(target_arch = "wasm32")]
136 return Ok(SendResult { reply_code: outcome.code });
137 #[cfg(not(target_arch = "wasm32"))]
138 Ok(SendResult { reply_code: _outcome.code })
139 }
140}
141
142#[cfg(target_arch = "wasm32")]
145impl Guest for SmtpSendImpl {
146 fn send(
147 config: SmtpConfig,
148 credentials: SmtpCredentials,
149 message: SmtpMessage,
150 ) -> Result<SendResult, SendError> {
151 Self::send_impl(config, credentials, message)
152 }
153}
154
155fn smtp_error_to_wit(e: wasm_smtp::SmtpError) -> SendError {
158 match e {
159 wasm_smtp::SmtpError::Io(io) => SendError::Io(io.to_string()),
160 wasm_smtp::SmtpError::Protocol(p) => SendError::Protocol(p.to_string()),
161 wasm_smtp::SmtpError::Auth(_) => SendError::AuthRejected,
162 wasm_smtp::SmtpError::InvalidInput(i) => SendError::InvalidInput(i.to_string()),
163 wasm_smtp::SmtpError::Policy(p) => SendError::PolicyRejected(p.to_string()),
164 }
165}
166
167#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn send_error_io_variant_carries_message() {
175 let e = SendError::Io("connection refused".into());
176 match e {
177 SendError::Io(msg) => assert_eq!(msg, "connection refused"),
178 _ => panic!("wrong variant"),
179 }
180 }
181
182 #[test]
183 fn send_error_auth_rejected_has_no_payload() {
184 let e = SendError::AuthRejected;
185 assert!(matches!(e, SendError::AuthRejected));
186 }
187
188 #[test]
189 fn smtp_config_fields_accessible() {
190 let cfg = SmtpConfig {
191 host: "smtp.example.com".into(),
192 port: 465,
193 ehlo_domain: "client.example.com".into(),
194 tls_mode: TlsMode::Implicit,
195 };
196 assert_eq!(cfg.host, "smtp.example.com");
197 assert_eq!(cfg.port, 465);
198 }
199
200 #[test]
201 fn smtp_message_to_is_vec() {
202 let msg = SmtpMessage {
203 from: "a@example.com".into(),
204 to: vec!["b@example.com".into(), "c@example.com".into()],
205 raw_message: "Subject: hi\r\n\r\nbody\r\n".into(),
206 };
207 assert_eq!(msg.to.len(), 2);
208 }
209
210 #[test]
211 fn credentials_fields_accessible() {
212 let cred = SmtpCredentials {
213 username: "user@example.com".into(),
214 password: "secret".into(),
215 };
216 assert_eq!(cred.username, "user@example.com");
217 assert!(!format!("{cred:?}").contains("secret"),
220 "credentials must not appear in Debug output");
221 }
222}