···210210 }
211211 }
212212213213+ // -- email smarthost --------------------------------------------------
214214+ match self.email.smarthost.tls.to_ascii_lowercase().as_str() {
215215+ "implicit" | "starttls" => {}
216216+ "none" => {
217217+ if self.email.smarthost.password.is_some() {
218218+ errors.push(
219219+ "email.smarthost.tls = \"none\" with email.smarthost.password set \
220220+ would transmit credentials in plaintext; use \"starttls\" or \"implicit\""
221221+ .to_string(),
222222+ );
223223+ }
224224+ }
225225+ other => errors.push(format!(
226226+ "email.smarthost.tls must be \"implicit\", \"starttls\", or \"none\", got \"{other}\""
227227+ )),
228228+ }
229229+230230+ let smarthost_host_set = self
231231+ .email
232232+ .smarthost
233233+ .host
234234+ .as_deref()
235235+ .is_some_and(|h| !h.is_empty());
236236+ let username_set = self.email.smarthost.username.is_some();
237237+ let password_set = self.email.smarthost.password.is_some();
238238+ if !smarthost_host_set && (username_set || password_set) {
239239+ errors.push(
240240+ "email.smarthost.username or email.smarthost.password is set but \
241241+ email.smarthost.host is empty; credentials would be silently ignored"
242242+ .to_string(),
243243+ );
244244+ }
245245+ if smarthost_host_set && username_set != password_set {
246246+ errors.push(
247247+ "email.smarthost.username and email.smarthost.password must both be set or \
248248+ both unset; otherwise authentication would silently degrade to anonymous"
249249+ .to_string(),
250250+ );
251251+ }
252252+253253+ if self.email.smarthost.command_timeout_secs == 0 {
254254+ errors.push("email.smarthost.command_timeout_secs must be at least 1".to_string());
255255+ }
256256+ if self.email.smarthost.total_timeout_secs == 0 {
257257+ errors.push("email.smarthost.total_timeout_secs must be at least 1".to_string());
258258+ }
259259+ if self.email.smarthost.pool_size == 0 {
260260+ errors.push("email.smarthost.pool_size must be at least 1".to_string());
261261+ }
262262+263263+ if self.email.direct_mx.max_concurrent_sends == 0 {
264264+ errors.push("email.direct_mx.max_concurrent_sends must be at least 1".to_string());
265265+ }
266266+ if self.email.direct_mx.command_timeout_secs == 0 {
267267+ errors.push("email.direct_mx.command_timeout_secs must be at least 1".to_string());
268268+ }
269269+ if self.email.direct_mx.total_timeout_secs == 0 {
270270+ errors.push("email.direct_mx.total_timeout_secs must be at least 1".to_string());
271271+ }
272272+273273+ let dkim_set = self.email.dkim.selector.is_some()
274274+ || self.email.dkim.domain.is_some()
275275+ || self.email.dkim.private_key_path.is_some();
276276+ if dkim_set {
277277+ if self.email.dkim.selector.is_none() {
278278+ errors
279279+ .push("email.dkim.selector is required when any DKIM field is set".to_string());
280280+ }
281281+ if self.email.dkim.domain.is_none() {
282282+ errors.push("email.dkim.domain is required when any DKIM field is set".to_string());
283283+ }
284284+ if self.email.dkim.private_key_path.is_none() {
285285+ errors.push(
286286+ "email.dkim.private_key_path is required when any DKIM field is set"
287287+ .to_string(),
288288+ );
289289+ }
290290+ }
291291+213292 // -- telegram ---------------------------------------------------------
214293 if self.telegram.bot_token.is_some() && self.telegram.webhook_secret.is_none() {
215294 errors.push(
···754833 #[config(env = "MAIL_FROM_NAME", default = "Tranquil PDS")]
755834 pub from_name: String,
756835757757- /// Path to the `sendmail` binary.
758758- #[config(env = "SENDMAIL_PATH", default = "/usr/sbin/sendmail")]
759759- pub sendmail_path: String,
836836+ /// HELO/EHLO name announced to remote SMTP servers. Applies to both
837837+ /// smarthost and direct-MX modes. Defaults to the server hostname.
838838+ #[config(env = "MAIL_HELO_NAME")]
839839+ pub helo_name: Option<String>,
840840+841841+ #[config(nested)]
842842+ pub smarthost: SmarthostConfig,
843843+844844+ #[config(nested)]
845845+ pub direct_mx: DirectMxConfig,
846846+847847+ #[config(nested)]
848848+ pub dkim: DkimConfig,
849849+}
850850+851851+#[derive(Debug, Config)]
852852+pub struct SmarthostConfig {
853853+ /// SMTP relay host. When set, mail is delivered through this host
854854+ /// instead of resolving recipient MX records directly.
855855+ #[config(env = "MAIL_SMARTHOST_HOST")]
856856+ pub host: Option<String>,
857857+858858+ /// SMTP relay port.
859859+ #[config(env = "MAIL_SMARTHOST_PORT", default = 587)]
860860+ pub port: u16,
861861+862862+ /// SMTP authentication username.
863863+ #[config(env = "MAIL_SMARTHOST_USERNAME")]
864864+ pub username: Option<String>,
865865+866866+ /// SMTP authentication password.
867867+ #[config(env = "MAIL_SMARTHOST_PASSWORD")]
868868+ pub password: Option<String>,
869869+870870+ /// TLS mode. Valid values: "implicit", "starttls", "none". Setting "none"
871871+ /// alongside a password is rejected at startup to prevent transmitting
872872+ /// credentials in plaintext.
873873+ #[config(env = "MAIL_SMARTHOST_TLS", default = "starttls")]
874874+ pub tls: String,
875875+876876+ /// Max size of the connection pool.
877877+ #[config(env = "MAIL_SMARTHOST_POOL_SIZE", default = 4)]
878878+ pub pool_size: u32,
879879+880880+ /// Per-command SMTP timeout in seconds. Bounds the security handshake.
881881+ #[config(env = "MAIL_SMARTHOST_COMMAND_TIMEOUT_SECS", default = 30)]
882882+ pub command_timeout_secs: u64,
883883+884884+ /// Total per-message timeout in seconds. Wraps the entire send so a
885885+ /// stuck relay cannot stall the comms queue.
886886+ #[config(env = "MAIL_SMARTHOST_TOTAL_TIMEOUT_SECS", default = 60)]
887887+ pub total_timeout_secs: u64,
888888+}
889889+890890+#[derive(Debug, Config)]
891891+pub struct DirectMxConfig {
892892+ /// Per-command SMTP timeout in seconds.
893893+ #[config(env = "MAIL_COMMAND_TIMEOUT_SECS", default = 30)]
894894+ pub command_timeout_secs: u64,
895895+896896+ /// Total per-message timeout across all MX attempts in seconds.
897897+ #[config(env = "MAIL_TOTAL_TIMEOUT_SECS", default = 60)]
898898+ pub total_timeout_secs: u64,
899899+900900+ /// Max number of concurrent direct-MX sends. Limits the load placed
901901+ /// on any single recipient MX during a backlog drain.
902902+ #[config(env = "MAIL_MAX_CONCURRENT_SENDS", default = 8)]
903903+ pub max_concurrent_sends: usize,
904904+905905+ /// Require STARTTLS on every MX hop. When false, TLS is
906906+ /// attempted opportunistically and the session falls back to plaintext
907907+ /// if the remote does not advertise STARTTLS. Set true to refuse
908908+ /// plaintext delivery, at the cost of failing sends to MX hosts that
909909+ /// do not support TLS.
910910+ #[config(env = "MAIL_REQUIRE_TLS", default = false)]
911911+ pub require_tls: bool,
912912+}
913913+914914+#[derive(Debug, Config)]
915915+pub struct DkimConfig {
916916+ /// DKIM selector. When unset, outgoing mail is not signed.
917917+ #[config(env = "MAIL_DKIM_SELECTOR")]
918918+ pub selector: Option<String>,
919919+920920+ /// DKIM signing domain.
921921+ #[config(env = "MAIL_DKIM_DOMAIN")]
922922+ pub domain: Option<String>,
923923+924924+ /// Path to the DKIM private key in PEM format. Supports RSA and
925925+ /// Ed25519 keys.
926926+ #[config(env = "MAIL_DKIM_KEY_PATH")]
927927+ pub private_key_path: Option<String>,
760928}
761929762930#[derive(Debug, Config)]
+109-4
example.toml
···373373# Default value: "Tranquil PDS"
374374#from_name = "Tranquil PDS"
375375376376-# Path to the `sendmail` binary.
376376+# HELO/EHLO name announced to remote SMTP servers. Applies to both
377377+# smarthost and direct-MX modes. Defaults to the server hostname.
378378+#
379379+# Can also be specified via environment variable `MAIL_HELO_NAME`.
380380+#helo_name =
381381+382382+[email.smarthost]
383383+# SMTP relay host. When set, mail is delivered through this host
384384+# instead of resolving recipient MX records directly.
385385+#
386386+# Can also be specified via environment variable `MAIL_SMARTHOST_HOST`.
387387+#host =
388388+389389+# SMTP relay port.
390390+#
391391+# Can also be specified via environment variable `MAIL_SMARTHOST_PORT`.
392392+#
393393+# Default value: 587
394394+#port = 587
395395+396396+# SMTP authentication username.
397397+#
398398+# Can also be specified via environment variable `MAIL_SMARTHOST_USERNAME`.
399399+#username =
400400+401401+# SMTP authentication password.
402402+#
403403+# Can also be specified via environment variable `MAIL_SMARTHOST_PASSWORD`.
404404+#password =
405405+406406+# TLS mode. Valid values: "implicit", "starttls", "none". Setting "none"
407407+# alongside a password is rejected at startup to prevent transmitting
408408+# credentials in plaintext.
409409+#
410410+# Can also be specified via environment variable `MAIL_SMARTHOST_TLS`.
411411+#
412412+# Default value: "starttls"
413413+#tls = "starttls"
414414+415415+# Max size of the connection pool.
416416+#
417417+# Can also be specified via environment variable `MAIL_SMARTHOST_POOL_SIZE`.
418418+#
419419+# Default value: 4
420420+#pool_size = 4
421421+422422+# Per-command SMTP timeout in seconds. Bounds the security handshake.
423423+#
424424+# Can also be specified via environment variable `MAIL_SMARTHOST_COMMAND_TIMEOUT_SECS`.
425425+#
426426+# Default value: 30
427427+#command_timeout_secs = 30
428428+429429+# Total per-message timeout in seconds. Wraps the entire send so a
430430+# stuck relay cannot stall the comms queue.
431431+#
432432+# Can also be specified via environment variable `MAIL_SMARTHOST_TOTAL_TIMEOUT_SECS`.
377433#
378378-# Can also be specified via environment variable `SENDMAIL_PATH`.
434434+# Default value: 60
435435+#total_timeout_secs = 60
436436+437437+[email.direct_mx]
438438+# Per-command SMTP timeout in seconds.
379439#
380380-# Default value: "/usr/sbin/sendmail"
381381-#sendmail_path = "/usr/sbin/sendmail"
440440+# Can also be specified via environment variable `MAIL_COMMAND_TIMEOUT_SECS`.
441441+#
442442+# Default value: 30
443443+#command_timeout_secs = 30
444444+445445+# Total per-message timeout across all MX attempts in seconds.
446446+#
447447+# Can also be specified via environment variable `MAIL_TOTAL_TIMEOUT_SECS`.
448448+#
449449+# Default value: 60
450450+#total_timeout_secs = 60
451451+452452+# Max number of concurrent direct-MX sends. Limits the load placed
453453+# on any single recipient MX during a backlog drain.
454454+#
455455+# Can also be specified via environment variable `MAIL_MAX_CONCURRENT_SENDS`.
456456+#
457457+# Default value: 8
458458+#max_concurrent_sends = 8
459459+460460+# Require STARTTLS on every MX hop. When false, TLS is
461461+# attempted opportunistically and the session falls back to plaintext
462462+# if the remote does not advertise STARTTLS. Set true to refuse
463463+# plaintext delivery, at the cost of failing sends to MX hosts that
464464+# do not support TLS.
465465+#
466466+# Can also be specified via environment variable `MAIL_REQUIRE_TLS`.
467467+#
468468+# Default value: false
469469+#require_tls = false
470470+471471+[email.dkim]
472472+# DKIM selector. When unset, outgoing mail is not signed.
473473+#
474474+# Can also be specified via environment variable `MAIL_DKIM_SELECTOR`.
475475+#selector =
476476+477477+# DKIM signing domain.
478478+#
479479+# Can also be specified via environment variable `MAIL_DKIM_DOMAIN`.
480480+#domain =
481481+482482+# Path to the DKIM private key in PEM format. Supports RSA and
483483+# Ed25519 keys.
484484+#
485485+# Can also be specified via environment variable `MAIL_DKIM_KEY_PATH`.
486486+#private_key_path =
382487383488[discord]
384489# Discord bot token. When unset, Discord integration is disabled.