mas_config/sections/
email.rs1#![allow(deprecated)]
8
9use std::{num::NonZeroU16, str::FromStr};
10
11use lettre::message::Mailbox;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize, de::Error};
14
15use super::ConfigurationSection;
16
17#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
19#[serde(rename_all = "lowercase")]
20pub enum EmailSmtpMode {
21 Plain,
23
24 StartTls,
26
27 Tls,
29}
30
31#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, Default)]
33#[serde(rename_all = "snake_case")]
34pub enum EmailTransportKind {
35 #[default]
37 Blackhole,
38
39 Smtp,
41
42 Sendmail,
44}
45
46fn default_email() -> String {
47 r#""Authentication Service" <root@localhost>"#.to_owned()
48}
49
50#[allow(clippy::unnecessary_wraps)]
51fn default_sendmail_command() -> Option<String> {
52 Some("sendmail".to_owned())
53}
54
55#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
57pub struct EmailConfig {
58 #[serde(default = "default_email")]
60 #[schemars(email)]
61 pub from: String,
62
63 #[serde(default = "default_email")]
65 #[schemars(email)]
66 pub reply_to: String,
67
68 transport: EmailTransportKind,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
73 mode: Option<EmailSmtpMode>,
74
75 #[serde(skip_serializing_if = "Option::is_none")]
77 #[schemars(with = "Option<crate::schema::Hostname>")]
78 hostname: Option<String>,
79
80 #[serde(skip_serializing_if = "Option::is_none")]
83 #[schemars(range(min = 1, max = 65535))]
84 port: Option<NonZeroU16>,
85
86 #[serde(skip_serializing_if = "Option::is_none")]
91 username: Option<String>,
92
93 #[serde(skip_serializing_if = "Option::is_none")]
98 password: Option<String>,
99
100 #[serde(skip_serializing_if = "Option::is_none")]
102 #[schemars(default = "default_sendmail_command")]
103 command: Option<String>,
104}
105
106impl EmailConfig {
107 #[must_use]
109 pub fn transport(&self) -> EmailTransportKind {
110 self.transport
111 }
112
113 #[must_use]
115 pub fn mode(&self) -> Option<EmailSmtpMode> {
116 self.mode
117 }
118
119 #[must_use]
121 pub fn hostname(&self) -> Option<&str> {
122 self.hostname.as_deref()
123 }
124
125 #[must_use]
127 pub fn port(&self) -> Option<NonZeroU16> {
128 self.port
129 }
130
131 #[must_use]
133 pub fn username(&self) -> Option<&str> {
134 self.username.as_deref()
135 }
136
137 #[must_use]
139 pub fn password(&self) -> Option<&str> {
140 self.password.as_deref()
141 }
142
143 #[must_use]
145 pub fn command(&self) -> Option<&str> {
146 self.command.as_deref()
147 }
148}
149
150impl Default for EmailConfig {
151 fn default() -> Self {
152 Self {
153 from: default_email(),
154 reply_to: default_email(),
155 transport: EmailTransportKind::Blackhole,
156 mode: None,
157 hostname: None,
158 port: None,
159 username: None,
160 password: None,
161 command: None,
162 }
163 }
164}
165
166impl ConfigurationSection for EmailConfig {
167 const PATH: Option<&'static str> = Some("email");
168
169 fn validate(
170 &self,
171 figment: &figment::Figment,
172 ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
173 let metadata = figment.find_metadata(Self::PATH.unwrap());
174
175 let error_on_field = |mut error: figment::error::Error, field: &'static str| {
176 error.metadata = metadata.cloned();
177 error.profile = Some(figment::Profile::Default);
178 error.path = vec![Self::PATH.unwrap().to_owned(), field.to_owned()];
179 error
180 };
181
182 let missing_field = |field: &'static str| {
183 error_on_field(figment::error::Error::missing_field(field), field)
184 };
185
186 let unexpected_field = |field: &'static str, expected_fields: &'static [&'static str]| {
187 error_on_field(
188 figment::error::Error::unknown_field(field, expected_fields),
189 field,
190 )
191 };
192
193 match self.transport {
194 EmailTransportKind::Blackhole => {}
195
196 EmailTransportKind::Smtp => {
197 if let Err(e) = Mailbox::from_str(&self.from) {
198 return Err(error_on_field(figment::error::Error::custom(e), "from").into());
199 }
200
201 if let Err(e) = Mailbox::from_str(&self.reply_to) {
202 return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into());
203 }
204
205 match (self.username.is_some(), self.password.is_some()) {
206 (true, true) | (false, false) => {}
207 (true, false) => {
208 return Err(missing_field("password").into());
209 }
210 (false, true) => {
211 return Err(missing_field("username").into());
212 }
213 }
214
215 if self.mode.is_none() {
216 return Err(missing_field("mode").into());
217 }
218
219 if self.hostname.is_none() {
220 return Err(missing_field("hostname").into());
221 }
222
223 if self.command.is_some() {
224 return Err(unexpected_field(
225 "command",
226 &[
227 "from",
228 "reply_to",
229 "transport",
230 "mode",
231 "hostname",
232 "port",
233 "username",
234 "password",
235 ],
236 )
237 .into());
238 }
239 }
240
241 EmailTransportKind::Sendmail => {
242 let expected_fields = &["from", "reply_to", "transport", "command"];
243
244 if let Err(e) = Mailbox::from_str(&self.from) {
245 return Err(error_on_field(figment::error::Error::custom(e), "from").into());
246 }
247
248 if let Err(e) = Mailbox::from_str(&self.reply_to) {
249 return Err(error_on_field(figment::error::Error::custom(e), "reply_to").into());
250 }
251
252 if self.command.is_none() {
253 return Err(missing_field("command").into());
254 }
255
256 if self.mode.is_some() {
257 return Err(unexpected_field("mode", expected_fields).into());
258 }
259
260 if self.hostname.is_some() {
261 return Err(unexpected_field("hostname", expected_fields).into());
262 }
263
264 if self.port.is_some() {
265 return Err(unexpected_field("port", expected_fields).into());
266 }
267
268 if self.username.is_some() {
269 return Err(unexpected_field("username", expected_fields).into());
270 }
271
272 if self.password.is_some() {
273 return Err(unexpected_field("password", expected_fields).into());
274 }
275 }
276 }
277
278 Ok(())
279 }
280}