@@ -19,7 +19,7 @@ use std::num::NonZeroU16;
19
19
use async_trait:: async_trait;
20
20
use rand:: Rng ;
21
21
use schemars:: JsonSchema ;
22
- use serde:: { Deserialize , Serialize } ;
22
+ use serde:: { de :: Error , Deserialize , Serialize } ;
23
23
24
24
use super :: ConfigurationSection ;
25
25
@@ -47,55 +47,27 @@ pub enum EmailSmtpMode {
47
47
}
48
48
49
49
/// What backend should be used when sending emails
50
- #[ derive( Clone , Debug , Serialize , Deserialize , JsonSchema ) ]
51
- #[ serde( tag = "transport" , rename_all = "snake_case" ) ]
52
- pub enum EmailTransportConfig {
50
+ #[ derive( Clone , Copy , Debug , Serialize , Deserialize , JsonSchema , Default ) ]
51
+ #[ serde( rename_all = "snake_case" ) ]
52
+ pub enum EmailTransportKind {
53
53
/// Don't send emails anywhere
54
+ #[ default]
54
55
Blackhole ,
55
56
56
57
/// Send emails via an SMTP relay
57
- Smtp {
58
- /// Connection mode to the relay
59
- mode : EmailSmtpMode ,
60
-
61
- /// Hostname to connect to
62
- #[ schemars( with = "crate::schema::Hostname" ) ]
63
- hostname : String ,
64
-
65
- /// Port to connect to. Default is 25 for plain, 465 for TLS and 587 for
66
- /// StartTLS
67
- #[ serde( default , skip_serializing_if = "Option::is_none" ) ]
68
- port : Option < NonZeroU16 > ,
69
-
70
- /// Set of credentials to use
71
- #[ serde( flatten, default ) ]
72
- credentials : Option < Credentials > ,
73
- } ,
58
+ Smtp ,
74
59
75
60
/// Send emails by calling sendmail
76
- Sendmail {
77
- /// Command to execute
78
- #[ serde( default = "default_sendmail_command" ) ]
79
- command : String ,
80
- } ,
81
-
82
- /// Send emails via the AWS SESv2 API
83
- #[ deprecated( note = "The AWS SESv2 backend has be removed." ) ]
84
- AwsSes ,
85
- }
86
-
87
- impl Default for EmailTransportConfig {
88
- fn default ( ) -> Self {
89
- Self :: Blackhole
90
- }
61
+ Sendmail ,
91
62
}
92
63
93
64
fn default_email ( ) -> String {
94
65
r#""Authentication Service" <root@localhost>"# . to_owned ( )
95
66
}
96
67
97
- fn default_sendmail_command ( ) -> String {
98
- "sendmail" . to_owned ( )
68
+ #[ allow( clippy:: unnecessary_wraps) ]
69
+ fn default_sendmail_command ( ) -> Option < String > {
70
+ Some ( "sendmail" . to_owned ( ) )
99
71
}
100
72
101
73
/// Configuration related to sending emails
@@ -112,16 +84,99 @@ pub struct EmailConfig {
112
84
pub reply_to : String ,
113
85
114
86
/// What backend should be used when sending emails
115
- #[ serde( flatten, default ) ]
116
- pub transport : EmailTransportConfig ,
87
+ transport : EmailTransportKind ,
88
+
89
+ /// SMTP transport: Connection mode to the relay
90
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
91
+ mode : Option < EmailSmtpMode > ,
92
+
93
+ /// SMTP transport: Hostname to connect to
94
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
95
+ #[ schemars( with = "Option<crate::schema::Hostname>" ) ]
96
+ hostname : Option < String > ,
97
+
98
+ /// SMTP transport: Port to connect to. Default is 25 for plain, 465 for TLS
99
+ /// and 587 for StartTLS
100
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
101
+ #[ schemars( range( min = 1 , max = 65535 ) ) ]
102
+ port : Option < NonZeroU16 > ,
103
+
104
+ /// SMTP transport: Username for use to authenticate when connecting to the
105
+ /// SMTP server
106
+ ///
107
+ /// Must be set if the `password` field is set
108
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
109
+ username : Option < String > ,
110
+
111
+ /// SMTP transport: Password for use to authenticate when connecting to the
112
+ /// SMTP server
113
+ ///
114
+ /// Must be set if the `username` field is set
115
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
116
+ password : Option < String > ,
117
+
118
+ /// Sendmail transport: Command to use to send emails
119
+ #[ serde( skip_serializing_if = "Option::is_none" ) ]
120
+ #[ schemars( default = "default_sendmail_command" ) ]
121
+ command : Option < String > ,
122
+ }
123
+
124
+ impl EmailConfig {
125
+ /// What backend should be used when sending emails
126
+ #[ must_use]
127
+ pub fn transport ( & self ) -> EmailTransportKind {
128
+ self . transport
129
+ }
130
+
131
+ /// Connection mode to the relay
132
+ #[ must_use]
133
+ pub fn mode ( & self ) -> Option < EmailSmtpMode > {
134
+ self . mode
135
+ }
136
+
137
+ /// Hostname to connect to
138
+ #[ must_use]
139
+ pub fn hostname ( & self ) -> Option < & str > {
140
+ self . hostname . as_deref ( )
141
+ }
142
+
143
+ /// Port to connect to
144
+ #[ must_use]
145
+ pub fn port ( & self ) -> Option < NonZeroU16 > {
146
+ self . port
147
+ }
148
+
149
+ /// Username for use to authenticate when connecting to the SMTP server
150
+ #[ must_use]
151
+ pub fn username ( & self ) -> Option < & str > {
152
+ self . username . as_deref ( )
153
+ }
154
+
155
+ /// Password for use to authenticate when connecting to the SMTP server
156
+ #[ must_use]
157
+ pub fn password ( & self ) -> Option < & str > {
158
+ self . password . as_deref ( )
159
+ }
160
+
161
+ /// Command to use to send emails
162
+ #[ must_use]
163
+ pub fn command ( & self ) -> Option < & str > {
164
+ self . command . as_deref ( )
165
+ }
117
166
}
118
167
119
168
impl Default for EmailConfig {
120
169
fn default ( ) -> Self {
121
170
Self {
122
171
from : default_email ( ) ,
123
172
reply_to : default_email ( ) ,
124
- transport : EmailTransportConfig :: Blackhole ,
173
+ transport : EmailTransportKind :: Blackhole ,
174
+ mode : None ,
175
+ hostname : None ,
176
+ port : None ,
177
+ username : None ,
178
+ password : None ,
179
+ command : None ,
125
180
}
126
181
}
127
182
}
@@ -137,6 +192,98 @@ impl ConfigurationSection for EmailConfig {
137
192
Ok ( Self :: default ( ) )
138
193
}
139
194
195
+ fn validate ( & self , figment : & figment:: Figment ) -> Result < ( ) , figment:: error:: Error > {
196
+ let metadata = figment. find_metadata ( Self :: PATH . unwrap ( ) ) ;
197
+
198
+ let error_on_field = |mut error : figment:: error:: Error , field : & ' static str | {
199
+ error. metadata = metadata. cloned ( ) ;
200
+ error. profile = Some ( figment:: Profile :: Default ) ;
201
+ error. path = vec ! [ Self :: PATH . unwrap( ) . to_owned( ) , field. to_owned( ) ] ;
202
+ error
203
+ } ;
204
+
205
+ let missing_field = |field : & ' static str | {
206
+ error_on_field ( figment:: error:: Error :: missing_field ( field) , field)
207
+ } ;
208
+
209
+ let unexpected_field = |field : & ' static str , expected_fields : & ' static [ & ' static str ] | {
210
+ error_on_field (
211
+ figment:: error:: Error :: unknown_field ( field, expected_fields) ,
212
+ field,
213
+ )
214
+ } ;
215
+
216
+ match self . transport {
217
+ EmailTransportKind :: Blackhole => { }
218
+
219
+ EmailTransportKind :: Smtp => {
220
+ match ( self . username . is_some ( ) , self . password . is_some ( ) ) {
221
+ ( true , true ) | ( false , false ) => { }
222
+ ( true , false ) => {
223
+ return Err ( missing_field ( "password" ) ) ;
224
+ }
225
+ ( false , true ) => {
226
+ return Err ( missing_field ( "username" ) ) ;
227
+ }
228
+ }
229
+
230
+ if self . mode . is_none ( ) {
231
+ return Err ( missing_field ( "mode" ) ) ;
232
+ }
233
+
234
+ if self . hostname . is_none ( ) {
235
+ return Err ( missing_field ( "hostname" ) ) ;
236
+ }
237
+
238
+ if self . command . is_some ( ) {
239
+ return Err ( unexpected_field (
240
+ "command" ,
241
+ & [
242
+ "from" ,
243
+ "reply_to" ,
244
+ "transport" ,
245
+ "mode" ,
246
+ "hostname" ,
247
+ "port" ,
248
+ "username" ,
249
+ "password" ,
250
+ ] ,
251
+ ) ) ;
252
+ }
253
+ }
254
+
255
+ EmailTransportKind :: Sendmail => {
256
+ let expected_fields = & [ "from" , "reply_to" , "transport" , "command" ] ;
257
+
258
+ if self . command . is_none ( ) {
259
+ return Err ( missing_field ( "command" ) ) ;
260
+ }
261
+
262
+ if self . mode . is_some ( ) {
263
+ return Err ( unexpected_field ( "mode" , expected_fields) ) ;
264
+ }
265
+
266
+ if self . hostname . is_some ( ) {
267
+ return Err ( unexpected_field ( "hostname" , expected_fields) ) ;
268
+ }
269
+
270
+ if self . port . is_some ( ) {
271
+ return Err ( unexpected_field ( "port" , expected_fields) ) ;
272
+ }
273
+
274
+ if self . username . is_some ( ) {
275
+ return Err ( unexpected_field ( "username" , expected_fields) ) ;
276
+ }
277
+
278
+ if self . password . is_some ( ) {
279
+ return Err ( unexpected_field ( "password" , expected_fields) ) ;
280
+ }
281
+ }
282
+ }
283
+
284
+ Ok ( ( ) )
285
+ }
286
+
140
287
fn test ( ) -> Self {
141
288
Self :: default ( )
142
289
}
0 commit comments