Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: c6d92b4de9d36806b82035df77c965530a95b2da
Timestamp: Sun, 12 Jan 2025 14:49:14 +0000 (1 week ago)

+3533 -4764 +/-43 browse
simplify delivery architecture considerably
simplify delivery architecture considerably

This simplifies the delivery architecture of Maitred considerably where only
a simple Delivery trait needs to be implemented to handle incoming mail. The
multi-threaded worker design has been eliminated completely and the caller is
now responsible for handling that.

The maitred-debug server has been removed in favour of just using examples
for testing.

A mail-auth based e-mail authentication module has been added.
1diff --git a/Cargo.lock b/Cargo.lock
2index d58ab30..77a6a43 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -4,20 +4,14 @@ version = 4
6
7 [[package]]
8 name = "addr2line"
9- version = "0.22.0"
10+ version = "0.24.2"
11 source = "registry+https://github.com/rust-lang/crates.io-index"
12- checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
13+ checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
14 dependencies = [
15 "gimli",
16 ]
17
18 [[package]]
19- name = "adler"
20- version = "1.0.2"
21- source = "registry+https://github.com/rust-lang/crates.io-index"
22- checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
23-
24- [[package]]
25 name = "adler2"
26 version = "2.0.0"
27 source = "registry+https://github.com/rust-lang/crates.io-index"
28 @@ -63,68 +57,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
29 checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
30
31 [[package]]
32- name = "anstream"
33- version = "0.6.15"
34- source = "registry+https://github.com/rust-lang/crates.io-index"
35- checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
36- dependencies = [
37- "anstyle",
38- "anstyle-parse",
39- "anstyle-query",
40- "anstyle-wincon",
41- "colorchoice",
42- "is_terminal_polyfill",
43- "utf8parse",
44- ]
45-
46- [[package]]
47- name = "anstyle"
48- version = "1.0.8"
49- source = "registry+https://github.com/rust-lang/crates.io-index"
50- checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
51-
52- [[package]]
53- name = "anstyle-parse"
54- version = "0.2.5"
55- source = "registry+https://github.com/rust-lang/crates.io-index"
56- checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
57- dependencies = [
58- "utf8parse",
59- ]
60-
61- [[package]]
62- name = "anstyle-query"
63- version = "1.1.1"
64- source = "registry+https://github.com/rust-lang/crates.io-index"
65- checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
66- dependencies = [
67- "windows-sys 0.52.0",
68- ]
69-
70- [[package]]
71- name = "anstyle-wincon"
72- version = "3.0.4"
73- source = "registry+https://github.com/rust-lang/crates.io-index"
74- checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
75- dependencies = [
76- "anstyle",
77- "windows-sys 0.52.0",
78- ]
79-
80- [[package]]
81 name = "arbitrary"
82- version = "1.3.2"
83+ version = "1.4.1"
84 source = "registry+https://github.com/rust-lang/crates.io-index"
85- checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
86+ checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
87 dependencies = [
88 "derive_arbitrary",
89 ]
90
91 [[package]]
92 name = "async-trait"
93- version = "0.1.83"
94+ version = "0.1.85"
95 source = "registry+https://github.com/rust-lang/crates.io-index"
96- checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
97+ checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
98 dependencies = [
99 "proc-macro2",
100 "quote",
101 @@ -133,50 +78,48 @@ dependencies = [
102
103 [[package]]
104 name = "autocfg"
105- version = "1.3.0"
106+ version = "1.4.0"
107 source = "registry+https://github.com/rust-lang/crates.io-index"
108- checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
109+ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
110
111 [[package]]
112 name = "aws-lc-rs"
113- version = "1.9.0"
114+ version = "1.12.0"
115 source = "registry+https://github.com/rust-lang/crates.io-index"
116- checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070"
117+ checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148"
118 dependencies = [
119 "aws-lc-sys",
120- "mirai-annotations",
121 "paste",
122 "zeroize",
123 ]
124
125 [[package]]
126 name = "aws-lc-sys"
127- version = "0.21.1"
128+ version = "0.24.1"
129 source = "registry+https://github.com/rust-lang/crates.io-index"
130- checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9"
131+ checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2"
132 dependencies = [
133 "bindgen",
134 "cc",
135 "cmake",
136 "dunce",
137 "fs_extra",
138- "libc",
139 "paste",
140 ]
141
142 [[package]]
143 name = "backtrace"
144- version = "0.3.73"
145+ version = "0.3.74"
146 source = "registry+https://github.com/rust-lang/crates.io-index"
147- checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
148+ checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
149 dependencies = [
150 "addr2line",
151- "cc",
152 "cfg-if",
153 "libc",
154- "miniz_oxide 0.7.4",
155+ "miniz_oxide",
156 "object",
157 "rustc-demangle",
158+ "windows-targets 0.52.6",
159 ]
160
161 [[package]]
162 @@ -199,9 +142,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
163
164 [[package]]
165 name = "bindgen"
166- version = "0.69.4"
167+ version = "0.69.5"
168 source = "registry+https://github.com/rust-lang/crates.io-index"
169- checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
170+ checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088"
171 dependencies = [
172 "bitflags",
173 "cexpr",
174 @@ -222,9 +165,9 @@ dependencies = [
175
176 [[package]]
177 name = "bitflags"
178- version = "2.6.0"
179+ version = "2.7.0"
180 source = "registry+https://github.com/rust-lang/crates.io-index"
181- checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
182+ checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be"
183
184 [[package]]
185 name = "block-buffer"
186 @@ -249,9 +192,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
187
188 [[package]]
189 name = "bytes"
190- version = "1.8.0"
191+ version = "1.9.0"
192 source = "registry+https://github.com/rust-lang/crates.io-index"
193- checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
194+ checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
195
196 [[package]]
197 name = "bzip2"
198 @@ -276,9 +219,9 @@ dependencies = [
199
200 [[package]]
201 name = "cc"
202- version = "1.2.5"
203+ version = "1.2.9"
204 source = "registry+https://github.com/rust-lang/crates.io-index"
205- checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
206+ checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b"
207 dependencies = [
208 "jobserver",
209 "libc",
210 @@ -316,7 +259,7 @@ version = "0.9.3"
211 source = "registry+https://github.com/rust-lang/crates.io-index"
212 checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
213 dependencies = [
214- "hashbrown",
215+ "hashbrown 0.14.5",
216 "stacker",
217 ]
218
219 @@ -342,61 +285,15 @@ dependencies = [
220 ]
221
222 [[package]]
223- name = "clap"
224- version = "4.5.20"
225- source = "registry+https://github.com/rust-lang/crates.io-index"
226- checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
227- dependencies = [
228- "clap_builder",
229- "clap_derive",
230- ]
231-
232- [[package]]
233- name = "clap_builder"
234- version = "4.5.20"
235- source = "registry+https://github.com/rust-lang/crates.io-index"
236- checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
237- dependencies = [
238- "anstream",
239- "anstyle",
240- "clap_lex",
241- "strsim",
242- ]
243-
244- [[package]]
245- name = "clap_derive"
246- version = "4.5.18"
247- source = "registry+https://github.com/rust-lang/crates.io-index"
248- checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
249- dependencies = [
250- "heck 0.5.0",
251- "proc-macro2",
252- "quote",
253- "syn",
254- ]
255-
256- [[package]]
257- name = "clap_lex"
258- version = "0.7.2"
259- source = "registry+https://github.com/rust-lang/crates.io-index"
260- checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
261-
262- [[package]]
263 name = "cmake"
264- version = "0.1.51"
265+ version = "0.1.52"
266 source = "registry+https://github.com/rust-lang/crates.io-index"
267- checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a"
268+ checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e"
269 dependencies = [
270 "cc",
271 ]
272
273 [[package]]
274- name = "colorchoice"
275- version = "1.0.2"
276- source = "registry+https://github.com/rust-lang/crates.io-index"
277- checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
278-
279- [[package]]
280 name = "const-oid"
281 version = "0.9.6"
282 source = "registry+https://github.com/rust-lang/crates.io-index"
283 @@ -410,9 +307,9 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
284
285 [[package]]
286 name = "cpufeatures"
287- version = "0.2.13"
288+ version = "0.2.16"
289 source = "registry+https://github.com/rust-lang/crates.io-index"
290- checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
291+ checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
292 dependencies = [
293 "libc",
294 ]
295 @@ -442,29 +339,10 @@ dependencies = [
296 ]
297
298 [[package]]
299- name = "crossbeam-deque"
300- version = "0.8.5"
301- source = "registry+https://github.com/rust-lang/crates.io-index"
302- checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
303- dependencies = [
304- "crossbeam-epoch",
305- "crossbeam-utils",
306- ]
307-
308- [[package]]
309- name = "crossbeam-epoch"
310- version = "0.9.18"
311- source = "registry+https://github.com/rust-lang/crates.io-index"
312- checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
313- dependencies = [
314- "crossbeam-utils",
315- ]
316-
317- [[package]]
318 name = "crossbeam-utils"
319- version = "0.8.20"
320+ version = "0.8.21"
321 source = "registry+https://github.com/rust-lang/crates.io-index"
322- checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
323+ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
324
325 [[package]]
326 name = "crypto-common"
327 @@ -537,9 +415,9 @@ dependencies = [
328
329 [[package]]
330 name = "derive_arbitrary"
331- version = "1.3.2"
332+ version = "1.4.1"
333 source = "registry+https://github.com/rust-lang/crates.io-index"
334- checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
335+ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
336 dependencies = [
337 "proc-macro2",
338 "quote",
339 @@ -626,20 +504,20 @@ dependencies = [
340
341 [[package]]
342 name = "encoding_rs"
343- version = "0.8.34"
344+ version = "0.8.35"
345 source = "registry+https://github.com/rust-lang/crates.io-index"
346- checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
347+ checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
348 dependencies = [
349 "cfg-if",
350 ]
351
352 [[package]]
353 name = "enum-as-inner"
354- version = "0.6.0"
355+ version = "0.6.1"
356 source = "registry+https://github.com/rust-lang/crates.io-index"
357- checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a"
358+ checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
359 dependencies = [
360- "heck 0.4.1",
361+ "heck",
362 "proc-macro2",
363 "quote",
364 "syn",
365 @@ -653,12 +531,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
366
367 [[package]]
368 name = "errno"
369- version = "0.3.9"
370+ version = "0.3.10"
371 source = "registry+https://github.com/rust-lang/crates.io-index"
372- checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
373+ checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
374 dependencies = [
375 "libc",
376- "windows-sys 0.52.0",
377+ "windows-sys 0.59.0",
378 ]
379
380 [[package]]
381 @@ -675,12 +553,12 @@ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
382
383 [[package]]
384 name = "flate2"
385- version = "1.0.33"
386+ version = "1.0.35"
387 source = "registry+https://github.com/rust-lang/crates.io-index"
388- checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253"
389+ checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
390 dependencies = [
391 "crc32fast",
392- "miniz_oxide 0.8.0",
393+ "miniz_oxide",
394 ]
395
396 [[package]]
397 @@ -830,15 +708,15 @@ dependencies = [
398
399 [[package]]
400 name = "gimli"
401- version = "0.29.0"
402+ version = "0.31.1"
403 source = "registry+https://github.com/rust-lang/crates.io-index"
404- checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
405+ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
406
407 [[package]]
408 name = "glob"
409- version = "0.3.1"
410+ version = "0.3.2"
411 source = "registry+https://github.com/rust-lang/crates.io-index"
412- checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
413+ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
414
415 [[package]]
416 name = "hashbrown"
417 @@ -851,10 +729,10 @@ dependencies = [
418 ]
419
420 [[package]]
421- name = "heck"
422- version = "0.4.1"
423+ name = "hashbrown"
424+ version = "0.15.2"
425 source = "registry+https://github.com/rust-lang/crates.io-index"
426- checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
427+ checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
428
429 [[package]]
430 name = "heck"
431 @@ -863,16 +741,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
432 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
433
434 [[package]]
435- name = "hermit-abi"
436- version = "0.3.9"
437- source = "registry+https://github.com/rust-lang/crates.io-index"
438- checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
439-
440- [[package]]
441 name = "hickory-proto"
442- version = "0.24.1"
443+ version = "0.24.2"
444 source = "registry+https://github.com/rust-lang/crates.io-index"
445- checksum = "07698b8420e2f0d6447a436ba999ec85d8fbf2a398bbd737b82cac4a2e96e512"
446+ checksum = "447afdcdb8afb9d0a852af6dc65d9b285ce720ed7a59e42a8bf2e931c67bc1b5"
447 dependencies = [
448 "async-trait",
449 "cfg-if",
450 @@ -881,14 +753,14 @@ dependencies = [
451 "futures-channel",
452 "futures-io",
453 "futures-util",
454- "idna 0.4.0",
455+ "idna",
456 "ipnet",
457 "once_cell",
458 "rand",
459 "ring 0.16.20",
460 "rustls 0.21.12",
461 "rustls-pemfile 1.0.4",
462- "thiserror",
463+ "thiserror 1.0.69",
464 "tinyvec",
465 "tokio",
466 "tokio-rustls 0.24.1",
467 @@ -913,7 +785,7 @@ dependencies = [
468 "resolv-conf",
469 "rustls 0.21.12",
470 "smallvec",
471- "thiserror",
472+ "thiserror 1.0.69",
473 "tokio",
474 "tokio-rustls 0.24.1",
475 "tracing",
476 @@ -930,11 +802,11 @@ dependencies = [
477
478 [[package]]
479 name = "home"
480- version = "0.5.9"
481+ version = "0.5.11"
482 source = "registry+https://github.com/rust-lang/crates.io-index"
483- checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
484+ checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
485 dependencies = [
486- "windows-sys 0.52.0",
487+ "windows-sys 0.59.0",
488 ]
489
490 [[package]]
491 @@ -1085,26 +957,6 @@ dependencies = [
492
493 [[package]]
494 name = "idna"
495- version = "0.4.0"
496- source = "registry+https://github.com/rust-lang/crates.io-index"
497- checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
498- dependencies = [
499- "unicode-bidi",
500- "unicode-normalization",
501- ]
502-
503- [[package]]
504- name = "idna"
505- version = "0.5.0"
506- source = "registry+https://github.com/rust-lang/crates.io-index"
507- checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
508- dependencies = [
509- "unicode-bidi",
510- "unicode-normalization",
511- ]
512-
513- [[package]]
514- name = "idna"
515 version = "1.0.3"
516 source = "registry+https://github.com/rust-lang/crates.io-index"
517 checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
518 @@ -1126,12 +978,12 @@ dependencies = [
519
520 [[package]]
521 name = "indexmap"
522- version = "2.5.0"
523+ version = "2.7.0"
524 source = "registry+https://github.com/rust-lang/crates.io-index"
525- checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
526+ checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
527 dependencies = [
528 "equivalent",
529- "hashbrown",
530+ "hashbrown 0.15.2",
531 ]
532
533 [[package]]
534 @@ -1157,15 +1009,9 @@ dependencies = [
535
536 [[package]]
537 name = "ipnet"
538- version = "2.9.0"
539- source = "registry+https://github.com/rust-lang/crates.io-index"
540- checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
541-
542- [[package]]
543- name = "is_terminal_polyfill"
544- version = "1.70.1"
545+ version = "2.10.1"
546 source = "registry+https://github.com/rust-lang/crates.io-index"
547- checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
548+ checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
549
550 [[package]]
551 name = "itertools"
552 @@ -1178,9 +1024,9 @@ dependencies = [
553
554 [[package]]
555 name = "itoa"
556- version = "1.0.11"
557+ version = "1.0.14"
558 source = "registry+https://github.com/rust-lang/crates.io-index"
559- checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
560+ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
561
562 [[package]]
563 name = "jobserver"
564 @@ -1193,10 +1039,11 @@ dependencies = [
565
566 [[package]]
567 name = "js-sys"
568- version = "0.3.70"
569+ version = "0.3.76"
570 source = "registry+https://github.com/rust-lang/crates.io-index"
571- checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
572+ checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
573 dependencies = [
574+ "once_cell",
575 "wasm-bindgen",
576 ]
577
578 @@ -1232,34 +1079,34 @@ dependencies = [
579 "futures-util",
580 "hostname 0.4.0",
581 "httpdate",
582- "idna 1.0.3",
583+ "idna",
584 "mime",
585 "nom",
586 "percent-encoding",
587 "quoted_printable",
588 "rsa",
589- "rustls 0.23.15",
590+ "rustls 0.23.21",
591 "rustls-pemfile 2.2.0",
592 "rustls-pki-types",
593 "sha2",
594 "socket2",
595 "tokio",
596- "tokio-rustls 0.26.0",
597+ "tokio-rustls 0.26.1",
598 "url",
599 "webpki-roots",
600 ]
601
602 [[package]]
603 name = "libc"
604- version = "0.2.155"
605+ version = "0.2.169"
606 source = "registry+https://github.com/rust-lang/crates.io-index"
607- checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
608+ checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
609
610 [[package]]
611 name = "libloading"
612- version = "0.8.5"
613+ version = "0.8.6"
614 source = "registry+https://github.com/rust-lang/crates.io-index"
615- checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
616+ checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
617 dependencies = [
618 "cfg-if",
619 "windows-targets 0.52.6",
620 @@ -1279,9 +1126,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
621
622 [[package]]
623 name = "linux-raw-sys"
624- version = "0.4.14"
625+ version = "0.4.15"
626 source = "registry+https://github.com/rust-lang/crates.io-index"
627- checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
628+ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
629
630 [[package]]
631 name = "litemap"
632 @@ -1398,7 +1245,6 @@ dependencies = [
633 "async-trait",
634 "base64 0.22.1",
635 "bytes",
636- "crossbeam-deque",
637 "email_address",
638 "futures",
639 "hickory-resolver",
640 @@ -1410,13 +1256,13 @@ dependencies = [
641 "md5",
642 "port_check",
643 "proxy-header",
644- "rustls 0.23.15",
645+ "rustls 0.23.21",
646 "rustls-pemfile 2.2.0",
647 "smtp-proto",
648 "stringprep",
649- "thiserror",
650+ "thiserror 1.0.69",
651 "tokio",
652- "tokio-rustls 0.26.0",
653+ "tokio-rustls 0.26.1",
654 "tokio-stream",
655 "tokio-util",
656 "tracing",
657 @@ -1425,22 +1271,6 @@ dependencies = [
658 ]
659
660 [[package]]
661- name = "maitred-debug"
662- version = "0.1.0"
663- dependencies = [
664- "async-trait",
665- "clap",
666- "futures",
667- "maildir",
668- "maitred",
669- "serde",
670- "tokio",
671- "toml",
672- "tracing",
673- "tracing-subscriber",
674- ]
675-
676- [[package]]
677 name = "match_cfg"
678 version = "0.1.0"
679 source = "registry+https://github.com/rust-lang/crates.io-index"
680 @@ -1472,41 +1302,25 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
681
682 [[package]]
683 name = "miniz_oxide"
684- version = "0.7.4"
685+ version = "0.8.2"
686 source = "registry+https://github.com/rust-lang/crates.io-index"
687- checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
688- dependencies = [
689- "adler",
690- ]
691-
692- [[package]]
693- name = "miniz_oxide"
694- version = "0.8.0"
695- source = "registry+https://github.com/rust-lang/crates.io-index"
696- checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
697+ checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
698 dependencies = [
699 "adler2",
700 ]
701
702 [[package]]
703 name = "mio"
704- version = "1.0.1"
705+ version = "1.0.3"
706 source = "registry+https://github.com/rust-lang/crates.io-index"
707- checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
708+ checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
709 dependencies = [
710- "hermit-abi",
711 "libc",
712 "wasi",
713 "windows-sys 0.52.0",
714 ]
715
716 [[package]]
717- name = "mirai-annotations"
718- version = "1.12.0"
719- source = "registry+https://github.com/rust-lang/crates.io-index"
720- checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1"
721-
722- [[package]]
723 name = "nom"
724 version = "7.1.3"
725 source = "registry+https://github.com/rust-lang/crates.io-index"
726 @@ -1581,18 +1395,18 @@ dependencies = [
727
728 [[package]]
729 name = "object"
730- version = "0.36.2"
731+ version = "0.36.7"
732 source = "registry+https://github.com/rust-lang/crates.io-index"
733- checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e"
734+ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
735 dependencies = [
736 "memchr",
737 ]
738
739 [[package]]
740 name = "once_cell"
741- version = "1.19.0"
742+ version = "1.20.2"
743 source = "registry+https://github.com/rust-lang/crates.io-index"
744- checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
745+ checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
746
747 [[package]]
748 name = "overload"
749 @@ -1656,9 +1470,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
750
751 [[package]]
752 name = "pin-project-lite"
753- version = "0.2.14"
754+ version = "0.2.16"
755 source = "registry+https://github.com/rust-lang/crates.io-index"
756- checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
757+ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
758
759 [[package]]
760 name = "pin-utils"
761 @@ -1689,9 +1503,9 @@ dependencies = [
762
763 [[package]]
764 name = "pkg-config"
765- version = "0.3.30"
766+ version = "0.3.31"
767 source = "registry+https://github.com/rust-lang/crates.io-index"
768- checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
769+ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
770
771 [[package]]
772 name = "port_check"
773 @@ -1716,9 +1530,9 @@ dependencies = [
774
775 [[package]]
776 name = "prettyplease"
777- version = "0.2.22"
778+ version = "0.2.28"
779 source = "registry+https://github.com/rust-lang/crates.io-index"
780- checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba"
781+ checksum = "924b9a625d6df5b74b0b3cfbb5669b3f62ddf3d46a677ce12b1945471b4ae5c3"
782 dependencies = [
783 "proc-macro2",
784 "syn",
785 @@ -1726,9 +1540,9 @@ dependencies = [
786
787 [[package]]
788 name = "proc-macro2"
789- version = "1.0.86"
790+ version = "1.0.93"
791 source = "registry+https://github.com/rust-lang/crates.io-index"
792- checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
793+ checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
794 dependencies = [
795 "unicode-ident",
796 ]
797 @@ -1756,9 +1570,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
798
799 [[package]]
800 name = "quick-xml"
801- version = "0.37.1"
802+ version = "0.37.2"
803 source = "registry+https://github.com/rust-lang/crates.io-index"
804- checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03"
805+ checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
806 dependencies = [
807 "memchr",
808 ]
809 @@ -1810,18 +1624,18 @@ dependencies = [
810
811 [[package]]
812 name = "redox_syscall"
813- version = "0.5.3"
814+ version = "0.5.8"
815 source = "registry+https://github.com/rust-lang/crates.io-index"
816- checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
817+ checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
818 dependencies = [
819 "bitflags",
820 ]
821
822 [[package]]
823 name = "regex"
824- version = "1.10.6"
825+ version = "1.11.1"
826 source = "registry+https://github.com/rust-lang/crates.io-index"
827- checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
828+ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
829 dependencies = [
830 "aho-corasick",
831 "memchr",
832 @@ -1831,9 +1645,9 @@ dependencies = [
833
834 [[package]]
835 name = "regex-automata"
836- version = "0.4.7"
837+ version = "0.4.9"
838 source = "registry+https://github.com/rust-lang/crates.io-index"
839- checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
840+ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
841 dependencies = [
842 "aho-corasick",
843 "memchr",
844 @@ -1842,9 +1656,9 @@ dependencies = [
845
846 [[package]]
847 name = "regex-syntax"
848- version = "0.8.4"
849+ version = "0.8.5"
850 source = "registry+https://github.com/rust-lang/crates.io-index"
851- checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
852+ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
853
854 [[package]]
855 name = "resolv-conf"
856 @@ -1929,15 +1743,15 @@ dependencies = [
857
858 [[package]]
859 name = "rustix"
860- version = "0.38.34"
861+ version = "0.38.43"
862 source = "registry+https://github.com/rust-lang/crates.io-index"
863- checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
864+ checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6"
865 dependencies = [
866 "bitflags",
867 "errno",
868 "libc",
869 "linux-raw-sys",
870- "windows-sys 0.52.0",
871+ "windows-sys 0.59.0",
872 ]
873
874 [[package]]
875 @@ -1954,9 +1768,9 @@ dependencies = [
876
877 [[package]]
878 name = "rustls"
879- version = "0.23.15"
880+ version = "0.23.21"
881 source = "registry+https://github.com/rust-lang/crates.io-index"
882- checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993"
883+ checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8"
884 dependencies = [
885 "aws-lc-rs",
886 "log",
887 @@ -1988,9 +1802,9 @@ dependencies = [
888
889 [[package]]
890 name = "rustls-pki-types"
891- version = "1.10.0"
892+ version = "1.10.1"
893 source = "registry+https://github.com/rust-lang/crates.io-index"
894- checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b"
895+ checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37"
896
897 [[package]]
898 name = "rustls-webpki"
899 @@ -2044,18 +1858,18 @@ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
900
901 [[package]]
902 name = "serde"
903- version = "1.0.213"
904+ version = "1.0.204"
905 source = "registry+https://github.com/rust-lang/crates.io-index"
906- checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
907+ checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
908 dependencies = [
909 "serde_derive",
910 ]
911
912 [[package]]
913 name = "serde_derive"
914- version = "1.0.213"
915+ version = "1.0.204"
916 source = "registry+https://github.com/rust-lang/crates.io-index"
917- checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
918+ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
919 dependencies = [
920 "proc-macro2",
921 "quote",
922 @@ -2064,9 +1878,9 @@ dependencies = [
923
924 [[package]]
925 name = "serde_json"
926- version = "1.0.127"
927+ version = "1.0.135"
928 source = "registry+https://github.com/rust-lang/crates.io-index"
929- checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad"
930+ checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
931 dependencies = [
932 "itoa",
933 "memchr",
934 @@ -2075,15 +1889,6 @@ dependencies = [
935 ]
936
937 [[package]]
938- name = "serde_spanned"
939- version = "0.6.7"
940- source = "registry+https://github.com/rust-lang/crates.io-index"
941- checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
942- dependencies = [
943- "serde",
944- ]
945-
946- [[package]]
947 name = "sha1"
948 version = "0.10.6"
949 source = "registry+https://github.com/rust-lang/crates.io-index"
950 @@ -2171,9 +1976,9 @@ dependencies = [
951
952 [[package]]
953 name = "socket2"
954- version = "0.5.7"
955+ version = "0.5.8"
956 source = "registry+https://github.com/rust-lang/crates.io-index"
957- checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
958+ checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
959 dependencies = [
960 "libc",
961 "windows-sys 0.52.0",
962 @@ -2209,15 +2014,15 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
963
964 [[package]]
965 name = "stacker"
966- version = "0.1.15"
967+ version = "0.1.17"
968 source = "registry+https://github.com/rust-lang/crates.io-index"
969- checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
970+ checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b"
971 dependencies = [
972 "cc",
973 "cfg-if",
974 "libc",
975 "psm",
976- "winapi",
977+ "windows-sys 0.59.0",
978 ]
979
980 [[package]]
981 @@ -2232,12 +2037,6 @@ dependencies = [
982 ]
983
984 [[package]]
985- name = "strsim"
986- version = "0.11.1"
987- source = "registry+https://github.com/rust-lang/crates.io-index"
988- checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
989-
990- [[package]]
991 name = "subtle"
992 version = "2.6.1"
993 source = "registry+https://github.com/rust-lang/crates.io-index"
994 @@ -2245,9 +2044,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
995
996 [[package]]
997 name = "syn"
998- version = "2.0.85"
999+ version = "2.0.96"
1000 source = "registry+https://github.com/rust-lang/crates.io-index"
1001- checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56"
1002+ checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
1003 dependencies = [
1004 "proc-macro2",
1005 "quote",
1006 @@ -2267,18 +2066,38 @@ dependencies = [
1007
1008 [[package]]
1009 name = "thiserror"
1010- version = "1.0.65"
1011+ version = "1.0.69"
1012+ source = "registry+https://github.com/rust-lang/crates.io-index"
1013+ checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1014+ dependencies = [
1015+ "thiserror-impl 1.0.69",
1016+ ]
1017+
1018+ [[package]]
1019+ name = "thiserror"
1020+ version = "2.0.11"
1021+ source = "registry+https://github.com/rust-lang/crates.io-index"
1022+ checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
1023+ dependencies = [
1024+ "thiserror-impl 2.0.11",
1025+ ]
1026+
1027+ [[package]]
1028+ name = "thiserror-impl"
1029+ version = "1.0.69"
1030 source = "registry+https://github.com/rust-lang/crates.io-index"
1031- checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
1032+ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1033 dependencies = [
1034- "thiserror-impl",
1035+ "proc-macro2",
1036+ "quote",
1037+ "syn",
1038 ]
1039
1040 [[package]]
1041 name = "thiserror-impl"
1042- version = "1.0.65"
1043+ version = "2.0.11"
1044 source = "registry+https://github.com/rust-lang/crates.io-index"
1045- checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
1046+ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
1047 dependencies = [
1048 "proc-macro2",
1049 "quote",
1050 @@ -2297,9 +2116,9 @@ dependencies = [
1051
1052 [[package]]
1053 name = "time"
1054- version = "0.3.36"
1055+ version = "0.3.37"
1056 source = "registry+https://github.com/rust-lang/crates.io-index"
1057- checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
1058+ checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21"
1059 dependencies = [
1060 "deranged",
1061 "num-conv",
1062 @@ -2326,9 +2145,9 @@ dependencies = [
1063
1064 [[package]]
1065 name = "tinyvec"
1066- version = "1.8.0"
1067+ version = "1.8.1"
1068 source = "registry+https://github.com/rust-lang/crates.io-index"
1069- checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938"
1070+ checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
1071 dependencies = [
1072 "tinyvec_macros",
1073 ]
1074 @@ -2341,9 +2160,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
1075
1076 [[package]]
1077 name = "tokio"
1078- version = "1.41.0"
1079+ version = "1.43.0"
1080 source = "registry+https://github.com/rust-lang/crates.io-index"
1081- checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
1082+ checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
1083 dependencies = [
1084 "backtrace",
1085 "bytes",
1086 @@ -2359,9 +2178,9 @@ dependencies = [
1087
1088 [[package]]
1089 name = "tokio-macros"
1090- version = "2.4.0"
1091+ version = "2.5.0"
1092 source = "registry+https://github.com/rust-lang/crates.io-index"
1093- checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
1094+ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
1095 dependencies = [
1096 "proc-macro2",
1097 "quote",
1098 @@ -2380,20 +2199,19 @@ dependencies = [
1099
1100 [[package]]
1101 name = "tokio-rustls"
1102- version = "0.26.0"
1103+ version = "0.26.1"
1104 source = "registry+https://github.com/rust-lang/crates.io-index"
1105- checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
1106+ checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37"
1107 dependencies = [
1108- "rustls 0.23.15",
1109- "rustls-pki-types",
1110+ "rustls 0.23.21",
1111 "tokio",
1112 ]
1113
1114 [[package]]
1115 name = "tokio-stream"
1116- version = "0.1.16"
1117+ version = "0.1.17"
1118 source = "registry+https://github.com/rust-lang/crates.io-index"
1119- checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1"
1120+ checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
1121 dependencies = [
1122 "futures-core",
1123 "pin-project-lite",
1124 @@ -2403,60 +2221,26 @@ dependencies = [
1125
1126 [[package]]
1127 name = "tokio-util"
1128- version = "0.7.12"
1129+ version = "0.7.13"
1130 source = "registry+https://github.com/rust-lang/crates.io-index"
1131- checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a"
1132+ checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078"
1133 dependencies = [
1134 "bytes",
1135 "futures-core",
1136 "futures-io",
1137 "futures-sink",
1138 "futures-util",
1139- "hashbrown",
1140+ "hashbrown 0.14.5",
1141 "pin-project-lite",
1142 "slab",
1143 "tokio",
1144 ]
1145
1146 [[package]]
1147- name = "toml"
1148- version = "0.8.19"
1149- source = "registry+https://github.com/rust-lang/crates.io-index"
1150- checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
1151- dependencies = [
1152- "serde",
1153- "serde_spanned",
1154- "toml_datetime",
1155- "toml_edit",
1156- ]
1157-
1158- [[package]]
1159- name = "toml_datetime"
1160- version = "0.6.8"
1161- source = "registry+https://github.com/rust-lang/crates.io-index"
1162- checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
1163- dependencies = [
1164- "serde",
1165- ]
1166-
1167- [[package]]
1168- name = "toml_edit"
1169- version = "0.22.20"
1170- source = "registry+https://github.com/rust-lang/crates.io-index"
1171- checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d"
1172- dependencies = [
1173- "indexmap",
1174- "serde",
1175- "serde_spanned",
1176- "toml_datetime",
1177- "winnow",
1178- ]
1179-
1180- [[package]]
1181 name = "tracing"
1182- version = "0.1.40"
1183+ version = "0.1.41"
1184 source = "registry+https://github.com/rust-lang/crates.io-index"
1185- checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
1186+ checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
1187 dependencies = [
1188 "log",
1189 "pin-project-lite",
1190 @@ -2466,9 +2250,9 @@ dependencies = [
1191
1192 [[package]]
1193 name = "tracing-attributes"
1194- version = "0.1.27"
1195+ version = "0.1.28"
1196 source = "registry+https://github.com/rust-lang/crates.io-index"
1197- checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
1198+ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
1199 dependencies = [
1200 "proc-macro2",
1201 "quote",
1202 @@ -2477,9 +2261,9 @@ dependencies = [
1203
1204 [[package]]
1205 name = "tracing-core"
1206- version = "0.1.32"
1207+ version = "0.1.33"
1208 source = "registry+https://github.com/rust-lang/crates.io-index"
1209- checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
1210+ checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
1211 dependencies = [
1212 "once_cell",
1213 "valuable",
1214 @@ -2498,9 +2282,9 @@ dependencies = [
1215
1216 [[package]]
1217 name = "tracing-subscriber"
1218- version = "0.3.18"
1219+ version = "0.3.19"
1220 source = "registry+https://github.com/rust-lang/crates.io-index"
1221- checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
1222+ checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008"
1223 dependencies = [
1224 "nu-ansi-term",
1225 "sharded-slab",
1226 @@ -2518,9 +2302,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
1227
1228 [[package]]
1229 name = "unicode-bidi"
1230- version = "0.3.15"
1231+ version = "0.3.18"
1232 source = "registry+https://github.com/rust-lang/crates.io-index"
1233- checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
1234+ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
1235
1236 [[package]]
1237 name = "unicode-ident"
1238 @@ -2530,18 +2314,18 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
1239
1240 [[package]]
1241 name = "unicode-normalization"
1242- version = "0.1.23"
1243+ version = "0.1.24"
1244 source = "registry+https://github.com/rust-lang/crates.io-index"
1245- checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
1246+ checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
1247 dependencies = [
1248 "tinyvec",
1249 ]
1250
1251 [[package]]
1252 name = "unicode-properties"
1253- version = "0.1.2"
1254+ version = "0.1.3"
1255 source = "registry+https://github.com/rust-lang/crates.io-index"
1256- checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
1257+ checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
1258
1259 [[package]]
1260 name = "untrusted"
1261 @@ -2557,12 +2341,12 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
1262
1263 [[package]]
1264 name = "url"
1265- version = "2.5.2"
1266+ version = "2.5.4"
1267 source = "registry+https://github.com/rust-lang/crates.io-index"
1268- checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
1269+ checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
1270 dependencies = [
1271 "form_urlencoded",
1272- "idna 0.5.0",
1273+ "idna",
1274 "percent-encoding",
1275 ]
1276
1277 @@ -2579,12 +2363,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1278 checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1279
1280 [[package]]
1281- name = "utf8parse"
1282- version = "0.2.2"
1283- source = "registry+https://github.com/rust-lang/crates.io-index"
1284- checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1285-
1286- [[package]]
1287 name = "valuable"
1288 version = "0.1.0"
1289 source = "registry+https://github.com/rust-lang/crates.io-index"
1290 @@ -2604,9 +2382,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
1291
1292 [[package]]
1293 name = "wasm-bindgen"
1294- version = "0.2.93"
1295+ version = "0.2.99"
1296 source = "registry+https://github.com/rust-lang/crates.io-index"
1297- checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
1298+ checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
1299 dependencies = [
1300 "cfg-if",
1301 "once_cell",
1302 @@ -2615,13 +2393,12 @@ dependencies = [
1303
1304 [[package]]
1305 name = "wasm-bindgen-backend"
1306- version = "0.2.93"
1307+ version = "0.2.99"
1308 source = "registry+https://github.com/rust-lang/crates.io-index"
1309- checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
1310+ checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
1311 dependencies = [
1312 "bumpalo",
1313 "log",
1314- "once_cell",
1315 "proc-macro2",
1316 "quote",
1317 "syn",
1318 @@ -2630,9 +2407,9 @@ dependencies = [
1319
1320 [[package]]
1321 name = "wasm-bindgen-macro"
1322- version = "0.2.93"
1323+ version = "0.2.99"
1324 source = "registry+https://github.com/rust-lang/crates.io-index"
1325- checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
1326+ checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
1327 dependencies = [
1328 "quote",
1329 "wasm-bindgen-macro-support",
1330 @@ -2640,9 +2417,9 @@ dependencies = [
1331
1332 [[package]]
1333 name = "wasm-bindgen-macro-support"
1334- version = "0.2.93"
1335+ version = "0.2.99"
1336 source = "registry+https://github.com/rust-lang/crates.io-index"
1337- checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
1338+ checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
1339 dependencies = [
1340 "proc-macro2",
1341 "quote",
1342 @@ -2653,15 +2430,15 @@ dependencies = [
1343
1344 [[package]]
1345 name = "wasm-bindgen-shared"
1346- version = "0.2.93"
1347+ version = "0.2.99"
1348 source = "registry+https://github.com/rust-lang/crates.io-index"
1349- checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
1350+ checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
1351
1352 [[package]]
1353 name = "web-sys"
1354- version = "0.3.70"
1355+ version = "0.3.76"
1356 source = "registry+https://github.com/rust-lang/crates.io-index"
1357- checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
1358+ checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
1359 dependencies = [
1360 "js-sys",
1361 "wasm-bindgen",
1362 @@ -2754,6 +2531,15 @@ dependencies = [
1363 ]
1364
1365 [[package]]
1366+ name = "windows-sys"
1367+ version = "0.59.0"
1368+ source = "registry+https://github.com/rust-lang/crates.io-index"
1369+ checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
1370+ dependencies = [
1371+ "windows-targets 0.52.6",
1372+ ]
1373+
1374+ [[package]]
1375 name = "windows-targets"
1376 version = "0.48.5"
1377 source = "registry+https://github.com/rust-lang/crates.io-index"
1378 @@ -2875,15 +2661,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
1379 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
1380
1381 [[package]]
1382- name = "winnow"
1383- version = "0.6.18"
1384- source = "registry+https://github.com/rust-lang/crates.io-index"
1385- checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f"
1386- dependencies = [
1387- "memchr",
1388- ]
1389-
1390- [[package]]
1391 name = "winreg"
1392 version = "0.50.0"
1393 source = "registry+https://github.com/rust-lang/crates.io-index"
1394 @@ -3015,9 +2792,9 @@ dependencies = [
1395
1396 [[package]]
1397 name = "zip"
1398- version = "2.2.0"
1399+ version = "2.2.2"
1400 source = "registry+https://github.com/rust-lang/crates.io-index"
1401- checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494"
1402+ checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45"
1403 dependencies = [
1404 "aes",
1405 "arbitrary",
1406 @@ -3035,7 +2812,7 @@ dependencies = [
1407 "pbkdf2",
1408 "rand",
1409 "sha1",
1410- "thiserror",
1411+ "thiserror 2.0.11",
1412 "time",
1413 "zeroize",
1414 "zopfli",
1415 diff --git a/Cargo.toml b/Cargo.toml
1416index ad4d22e..7170f7e 100644
1417--- a/Cargo.toml
1418+++ b/Cargo.toml
1419 @@ -1,7 +1,73 @@
1420- [workspace]
1421- resolver = "2"
1422+ [package]
1423+ name = "maitred"
1424+ version = "0.1.0"
1425+ edition = "2021"
1426
1427- members = [
1428- "maitred",
1429- "maitred-debug"
1430+ [dependencies]
1431+ async-trait = "0.1.83"
1432+ base64 = { version = "0.22.1"}
1433+ bytes = "1.8.0"
1434+ email_address = "0.2.9"
1435+ futures = "0.3.31"
1436+ hickory-resolver = { version = "0.24.2", optional = true }
1437+ mail-auth = { version = "0.5.1", features = ["ring", "rustls-pemfile"], optional = true }
1438+ mail-builder = "0.3.2"
1439+ mail-parser = { version = "0.9.4", features = ["serde", "serde_support"] }
1440+ maildir = "0.6.4"
1441+ md5 = "0.7.0"
1442+ proxy-header = "0.1.2"
1443+ rustls = { version = "0.23.15", optional = true }
1444+ rustls-pemfile = { version = "2.2.0", optional = true }
1445+ smtp-proto = { version = "0.1.5", features = ["serde", "serde_support"] }
1446+ stringprep = "0.1.5"
1447+ thiserror = "1.0.65"
1448+ tokio = { version = "1.41.0", features = ["full"], optional = true }
1449+ tokio-rustls = { version = "0.26.0", optional = true }
1450+ tokio-stream = { version = "0.1.16", features = ["full"], optional = true }
1451+ tokio-util = { version = "0.7.12", features = ["full"], optional = true }
1452+ tracing = { version = "0.1.40", features = ["log"] }
1453+ url = "2.5.2"
1454+
1455+ [dependencies.lettre]
1456+ version = "0.11.11"
1457+ features = ["dkim", "rustls-tls", "tokio1", "tokio1-rustls-tls", "builder", "hostname", "pool", "smtp-transport"]
1458+ optional = true
1459+ default-features = false
1460+
1461+ [dev-dependencies]
1462+ port_check = "0.2.1"
1463+ tracing-subscriber = "0.3.18"
1464+
1465+ [features]
1466+ default = []
1467+ full = ["relay", "server"]
1468+ server = [
1469+ "rustls",
1470+ "rustls-pemfile",
1471+ "tokio",
1472+ "tokio-rustls",
1473+ "tokio-stream",
1474+ "tokio-util"
1475+ ]
1476+ relay = [
1477+ "hickory-resolver",
1478+ "lettre",
1479+ "rustls"
1480 ]
1481+ authentication = [
1482+ "mail-auth"
1483+ ]
1484+
1485+ [[example]]
1486+ name = "session"
1487+ path = "examples/session.rs"
1488+
1489+ [[example]]
1490+ name = "server"
1491+ path = "examples/server.rs"
1492+ required-features = ["server"]
1493+
1494+ [[example]]
1495+ name = "relay"
1496+ path = "examples/relay.rs"
1497+ required-features = ["relay"]
1498 diff --git a/contrib/block-list.py b/contrib/block-list.py
1499new file mode 100755
1500index 0000000..e119b81
1501--- /dev/null
1502+++ b/contrib/block-list.py
1503 @@ -0,0 +1,13 @@
1504+ #!/usr/bin/env python
1505+
1506+ import requests
1507+
1508+ block_list = "https://github.com/borestad/blocklist-abuseipdb/raw/refs/heads/main/abuseipdb-s100-7d.ipv4"
1509+
1510+ if __name__ == "__main__":
1511+ response = requests.get(block_list)
1512+ for line in response.text.splitlines():
1513+ if line.startswith("#"):
1514+ continue
1515+ line = line.strip()
1516+ print(f'deny {line};')
1517 diff --git a/examples/relay.rs b/examples/relay.rs
1518new file mode 100644
1519index 0000000..e18b287
1520--- /dev/null
1521+++ b/examples/relay.rs
1522 @@ -0,0 +1,55 @@
1523+ use mail_parser::MessageParser;
1524+ use maitred::relay::{Error, Relay, Sorted};
1525+
1526+ const DNS_RESOLUTION_ENABLED: bool = false;
1527+
1528+ const TEST_EMAIL: &str = r#"From: hello@ayllu-forge.org
1529+ To: dev@localhost
1530+ Cc: Fuu Bar <kevin@ayllu-forge.org>
1531+ Subject: [PATCH] add delivery parameters for mail module in db crate
1532+ Date: Mon, 23 Dec 2024 18:49:34 +0100
1533+ Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
1534+ X-Mailer: git-send-email 2.47.1
1535+ MIME-Version: 1.0
1536+ Content-Transfer-Encoding: 8bit
1537+
1538+ From: Fuu Bar <me@example.org>
1539+
1540+ ---
1541+ ayllu-mail/src/delivery.rs | 12 +++++-----
1542+
1543+ TRUNCATED
1544+ "#;
1545+
1546+ #[tokio::main]
1547+ async fn main() {
1548+ tracing_subscriber::fmt()
1549+ .compact()
1550+ .with_line_number(true)
1551+ .init();
1552+ maitred::crypto::init();
1553+ let parser = MessageParser::new();
1554+ let message = parser.parse(TEST_EMAIL).unwrap();
1555+ let sorted = Sorted::from_message(&message).unwrap();
1556+ let relay = Relay::builder()
1557+ .port(2525)
1558+ .resolve_dns(DNS_RESOLUTION_ENABLED)
1559+ .build();
1560+ for (domain, envelope) in sorted.0.iter() {
1561+ println!("Delivering message to domain: {}", domain);
1562+ match relay.send(domain, envelope, message.raw_message()).await {
1563+ Ok(_) => {
1564+ println!("Message delivered successfully");
1565+ }
1566+ Err(Error::LettreTransport(errors)) => {
1567+ eprintln!("All delivery attempts failed:");
1568+ for (i, attempt) in errors.iter().enumerate() {
1569+ eprintln!("\tAttempt {}: {}", i, attempt);
1570+ }
1571+ }
1572+ Err(e) => {
1573+ eprintln!("Failed to send message: {}", e);
1574+ }
1575+ }
1576+ }
1577+ }
1578 diff --git a/examples/server.rs b/examples/server.rs
1579new file mode 100644
1580index 0000000..11347e9
1581--- /dev/null
1582+++ b/examples/server.rs
1583 @@ -0,0 +1,30 @@
1584+ /// Simple SMTP server that prints delivered messages to stdout.
1585+ use maitred::{DeliveryFunc, Envelope, server::Server};
1586+ use std::str::FromStr;
1587+ use tracing::Level;
1588+
1589+ const LISTEN_ADDR: &str = "127.0.0.1:2525";
1590+ const LOG_LEVEL: &str = "INFO";
1591+
1592+ #[tokio::main]
1593+ async fn main() {
1594+ tracing_subscriber::fmt()
1595+ .compact()
1596+ .with_line_number(true)
1597+ .with_max_level(Level::from_str(LOG_LEVEL).unwrap())
1598+ .init();
1599+
1600+ let mut mail_server = Server::default()
1601+ .address(LISTEN_ADDR)
1602+ .with_delivery(DeliveryFunc(|envelope: &Envelope| {
1603+ println!("From: {}", envelope.mail_from);
1604+ for to in envelope.rcpt_to.iter() {
1605+ println!("To: {}", to);
1606+ }
1607+ let raw_message = envelope.body.raw_message();
1608+ println!("Message:\n{}", String::from_utf8_lossy(raw_message));
1609+ async move { Ok(()) }
1610+ }));
1611+
1612+ mail_server.listen().await.unwrap();
1613+ }
1614 diff --git a/examples/session.rs b/examples/session.rs
1615new file mode 100644
1616index 0000000..85a974b
1617--- /dev/null
1618+++ b/examples/session.rs
1619 @@ -0,0 +1,112 @@
1620+ use std::net::IpAddr;
1621+ use std::str::FromStr;
1622+
1623+ /// An example of using a low level session object.
1624+ use maitred::{Action, Response, Session};
1625+ use smtp_proto::{MailFrom, RcptTo, Request};
1626+
1627+ const TEST_EMAIL: &str = r#"From: hello@example.com
1628+ To: dev@ayllu-dev.local
1629+ Cc: Fuu Bar <me@example.org>
1630+ Subject: [PATCH] add delivery parameters for mail module in db crate
1631+ Date: Mon, 23 Dec 2024 18:49:34 +0100
1632+ Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
1633+ X-Mailer: git-send-email 2.47.1
1634+ MIME-Version: 1.0
1635+ Content-Transfer-Encoding: 8bit
1636+
1637+ From: Fuu Bar <me@example.org>
1638+
1639+ ---
1640+ ayllu-mail/src/delivery.rs | 12 +++++-----
1641+
1642+ TRUNCATED
1643+ "#;
1644+
1645+ fn print_response(res: &Response<String>) {
1646+ match res {
1647+ Response::General(response) => {
1648+ println!(
1649+ "{} ({}, {}, {}): {}",
1650+ response.code(),
1651+ response.esc[0],
1652+ response.esc[1],
1653+ response.esc[2],
1654+ response.message
1655+ );
1656+ }
1657+ Response::Ehlo(ehlo_response) => {
1658+ println!("EHLO: {:?}", ehlo_response);
1659+ }
1660+ }
1661+ }
1662+
1663+ fn next(action: Action) {
1664+ match action {
1665+ Action::Send(response) => {
1666+ print_response(&response);
1667+ }
1668+ Action::SendMany(vec) => {
1669+ vec.iter().for_each(print_response);
1670+ }
1671+ Action::Message {
1672+ initial_response,
1673+ cb,
1674+ } => {
1675+ print_response(&initial_response);
1676+ next(cb.call(IpAddr::from_str("127.0.0.1").unwrap(), TEST_EMAIL.as_bytes()));
1677+ }
1678+ Action::Envelope {
1679+ initial_response,
1680+ envelope,
1681+ } => {
1682+ print_response(&initial_response);
1683+ println!(
1684+ "Message processed:\n{}",
1685+ String::from_utf8_lossy(envelope.body.raw_message())
1686+ );
1687+ }
1688+ Action::PlainAuth {
1689+ authcid: _,
1690+ authzid: _,
1691+ password: _,
1692+ cb: _,
1693+ } => todo!(),
1694+ Action::Verify { address: _, cb: _ } => todo!(),
1695+ Action::Expand { address: _, cb: _ } => todo!(),
1696+ Action::StartTls(_response) => todo!(),
1697+ Action::Quit(response) => {
1698+ print_response(&response);
1699+ }
1700+ }
1701+ }
1702+
1703+ fn main() {
1704+ let mut session = Session::default().our_hostname("example.org");
1705+ let requests: Vec<Option<Request<String>>> = vec![
1706+ None, // Initial greeting
1707+ Some(Request::Ehlo {
1708+ host: String::from("example.com"),
1709+ }),
1710+ Some(Request::Noop {
1711+ value: String::default(),
1712+ }),
1713+ Some(Request::Mail {
1714+ from: MailFrom {
1715+ address: String::from("hello@example.org"),
1716+ ..Default::default()
1717+ },
1718+ }),
1719+ Some(Request::Rcpt {
1720+ to: RcptTo {
1721+ address: String::from("dev@ayllu-dev.local"),
1722+ ..Default::default()
1723+ },
1724+ }),
1725+ Some(Request::Data), // Initiate data transfer
1726+ Some(Request::Quit),
1727+ ];
1728+ requests.iter().for_each(|req| {
1729+ next(session.next(req.as_ref()));
1730+ })
1731+ }
1732 diff --git a/maitred-debug/.gitignore b/maitred-debug/.gitignore
1733deleted file mode 100644
1734index ea8c4bf..0000000
1735--- a/maitred-debug/.gitignore
1736+++ /dev/null
1737 @@ -1 +0,0 @@
1738- /target
1739 diff --git a/maitred-debug/Cargo.lock b/maitred-debug/Cargo.lock
1740deleted file mode 100644
1741index 661ff90..0000000
1742--- a/maitred-debug/Cargo.lock
1743+++ /dev/null
1744 @@ -1,531 +0,0 @@
1745- # This file is automatically @generated by Cargo.
1746- # It is not intended for manual editing.
1747- version = 3
1748-
1749- [[package]]
1750- name = "addr2line"
1751- version = "0.22.0"
1752- source = "registry+https://github.com/rust-lang/crates.io-index"
1753- checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678"
1754- dependencies = [
1755- "gimli",
1756- ]
1757-
1758- [[package]]
1759- name = "adler"
1760- version = "1.0.2"
1761- source = "registry+https://github.com/rust-lang/crates.io-index"
1762- checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
1763-
1764- [[package]]
1765- name = "ahash"
1766- version = "0.8.11"
1767- source = "registry+https://github.com/rust-lang/crates.io-index"
1768- checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
1769- dependencies = [
1770- "cfg-if",
1771- "once_cell",
1772- "version_check",
1773- "zerocopy",
1774- ]
1775-
1776- [[package]]
1777- name = "allocator-api2"
1778- version = "0.2.18"
1779- source = "registry+https://github.com/rust-lang/crates.io-index"
1780- checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
1781-
1782- [[package]]
1783- name = "autocfg"
1784- version = "1.3.0"
1785- source = "registry+https://github.com/rust-lang/crates.io-index"
1786- checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
1787-
1788- [[package]]
1789- name = "backtrace"
1790- version = "0.3.73"
1791- source = "registry+https://github.com/rust-lang/crates.io-index"
1792- checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a"
1793- dependencies = [
1794- "addr2line",
1795- "cc",
1796- "cfg-if",
1797- "libc",
1798- "miniz_oxide",
1799- "object",
1800- "rustc-demangle",
1801- ]
1802-
1803- [[package]]
1804- name = "bitflags"
1805- version = "2.6.0"
1806- source = "registry+https://github.com/rust-lang/crates.io-index"
1807- checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
1808-
1809- [[package]]
1810- name = "bytes"
1811- version = "1.6.1"
1812- source = "registry+https://github.com/rust-lang/crates.io-index"
1813- checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952"
1814-
1815- [[package]]
1816- name = "cc"
1817- version = "1.1.6"
1818- source = "registry+https://github.com/rust-lang/crates.io-index"
1819- checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
1820-
1821- [[package]]
1822- name = "cfg-if"
1823- version = "1.0.0"
1824- source = "registry+https://github.com/rust-lang/crates.io-index"
1825- checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
1826-
1827- [[package]]
1828- name = "futures-core"
1829- version = "0.3.30"
1830- source = "registry+https://github.com/rust-lang/crates.io-index"
1831- checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
1832-
1833- [[package]]
1834- name = "futures-io"
1835- version = "0.3.30"
1836- source = "registry+https://github.com/rust-lang/crates.io-index"
1837- checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
1838-
1839- [[package]]
1840- name = "futures-macro"
1841- version = "0.3.30"
1842- source = "registry+https://github.com/rust-lang/crates.io-index"
1843- checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
1844- dependencies = [
1845- "proc-macro2",
1846- "quote",
1847- "syn",
1848- ]
1849-
1850- [[package]]
1851- name = "futures-sink"
1852- version = "0.3.30"
1853- source = "registry+https://github.com/rust-lang/crates.io-index"
1854- checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
1855-
1856- [[package]]
1857- name = "futures-task"
1858- version = "0.3.30"
1859- source = "registry+https://github.com/rust-lang/crates.io-index"
1860- checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
1861-
1862- [[package]]
1863- name = "futures-util"
1864- version = "0.3.30"
1865- source = "registry+https://github.com/rust-lang/crates.io-index"
1866- checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
1867- dependencies = [
1868- "futures-core",
1869- "futures-macro",
1870- "futures-task",
1871- "pin-project-lite",
1872- "pin-utils",
1873- "slab",
1874- ]
1875-
1876- [[package]]
1877- name = "gimli"
1878- version = "0.29.0"
1879- source = "registry+https://github.com/rust-lang/crates.io-index"
1880- checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
1881-
1882- [[package]]
1883- name = "hashbrown"
1884- version = "0.14.5"
1885- source = "registry+https://github.com/rust-lang/crates.io-index"
1886- checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
1887- dependencies = [
1888- "ahash",
1889- "allocator-api2",
1890- ]
1891-
1892- [[package]]
1893- name = "hermit-abi"
1894- version = "0.3.9"
1895- source = "registry+https://github.com/rust-lang/crates.io-index"
1896- checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
1897-
1898- [[package]]
1899- name = "libc"
1900- version = "0.2.155"
1901- source = "registry+https://github.com/rust-lang/crates.io-index"
1902- checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
1903-
1904- [[package]]
1905- name = "lock_api"
1906- version = "0.4.12"
1907- source = "registry+https://github.com/rust-lang/crates.io-index"
1908- checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
1909- dependencies = [
1910- "autocfg",
1911- "scopeguard",
1912- ]
1913-
1914- [[package]]
1915- name = "maitred"
1916- version = "0.1.0"
1917- dependencies = [
1918- "smtp-proto",
1919- ]
1920-
1921- [[package]]
1922- name = "maitred-debug"
1923- version = "0.1.0"
1924- dependencies = [
1925- "maitred",
1926- "tokio",
1927- "tokio-util",
1928- ]
1929-
1930- [[package]]
1931- name = "memchr"
1932- version = "2.7.4"
1933- source = "registry+https://github.com/rust-lang/crates.io-index"
1934- checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
1935-
1936- [[package]]
1937- name = "miniz_oxide"
1938- version = "0.7.4"
1939- source = "registry+https://github.com/rust-lang/crates.io-index"
1940- checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08"
1941- dependencies = [
1942- "adler",
1943- ]
1944-
1945- [[package]]
1946- name = "mio"
1947- version = "1.0.1"
1948- source = "registry+https://github.com/rust-lang/crates.io-index"
1949- checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4"
1950- dependencies = [
1951- "hermit-abi",
1952- "libc",
1953- "wasi",
1954- "windows-sys",
1955- ]
1956-
1957- [[package]]
1958- name = "object"
1959- version = "0.36.2"
1960- source = "registry+https://github.com/rust-lang/crates.io-index"
1961- checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e"
1962- dependencies = [
1963- "memchr",
1964- ]
1965-
1966- [[package]]
1967- name = "once_cell"
1968- version = "1.19.0"
1969- source = "registry+https://github.com/rust-lang/crates.io-index"
1970- checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
1971-
1972- [[package]]
1973- name = "parking_lot"
1974- version = "0.12.3"
1975- source = "registry+https://github.com/rust-lang/crates.io-index"
1976- checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
1977- dependencies = [
1978- "lock_api",
1979- "parking_lot_core",
1980- ]
1981-
1982- [[package]]
1983- name = "parking_lot_core"
1984- version = "0.9.10"
1985- source = "registry+https://github.com/rust-lang/crates.io-index"
1986- checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
1987- dependencies = [
1988- "cfg-if",
1989- "libc",
1990- "redox_syscall",
1991- "smallvec",
1992- "windows-targets",
1993- ]
1994-
1995- [[package]]
1996- name = "pin-project-lite"
1997- version = "0.2.14"
1998- source = "registry+https://github.com/rust-lang/crates.io-index"
1999- checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
2000-
2001- [[package]]
2002- name = "pin-utils"
2003- version = "0.1.0"
2004- source = "registry+https://github.com/rust-lang/crates.io-index"
2005- checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
2006-
2007- [[package]]
2008- name = "proc-macro2"
2009- version = "1.0.86"
2010- source = "registry+https://github.com/rust-lang/crates.io-index"
2011- checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
2012- dependencies = [
2013- "unicode-ident",
2014- ]
2015-
2016- [[package]]
2017- name = "quote"
2018- version = "1.0.36"
2019- source = "registry+https://github.com/rust-lang/crates.io-index"
2020- checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
2021- dependencies = [
2022- "proc-macro2",
2023- ]
2024-
2025- [[package]]
2026- name = "redox_syscall"
2027- version = "0.5.3"
2028- source = "registry+https://github.com/rust-lang/crates.io-index"
2029- checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4"
2030- dependencies = [
2031- "bitflags",
2032- ]
2033-
2034- [[package]]
2035- name = "rustc-demangle"
2036- version = "0.1.24"
2037- source = "registry+https://github.com/rust-lang/crates.io-index"
2038- checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
2039-
2040- [[package]]
2041- name = "scopeguard"
2042- version = "1.2.0"
2043- source = "registry+https://github.com/rust-lang/crates.io-index"
2044- checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
2045-
2046- [[package]]
2047- name = "serde"
2048- version = "1.0.204"
2049- source = "registry+https://github.com/rust-lang/crates.io-index"
2050- checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
2051- dependencies = [
2052- "serde_derive",
2053- ]
2054-
2055- [[package]]
2056- name = "serde_derive"
2057- version = "1.0.204"
2058- source = "registry+https://github.com/rust-lang/crates.io-index"
2059- checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
2060- dependencies = [
2061- "proc-macro2",
2062- "quote",
2063- "syn",
2064- ]
2065-
2066- [[package]]
2067- name = "signal-hook-registry"
2068- version = "1.4.2"
2069- source = "registry+https://github.com/rust-lang/crates.io-index"
2070- checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
2071- dependencies = [
2072- "libc",
2073- ]
2074-
2075- [[package]]
2076- name = "slab"
2077- version = "0.4.9"
2078- source = "registry+https://github.com/rust-lang/crates.io-index"
2079- checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
2080- dependencies = [
2081- "autocfg",
2082- ]
2083-
2084- [[package]]
2085- name = "smallvec"
2086- version = "1.13.2"
2087- source = "registry+https://github.com/rust-lang/crates.io-index"
2088- checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
2089-
2090- [[package]]
2091- name = "smtp-proto"
2092- version = "0.1.5"
2093- source = "registry+https://github.com/rust-lang/crates.io-index"
2094- checksum = "51b8ad3dd187f0d4debab02ad65405a9919d6a4f7bce25bd64a258781063a53a"
2095- dependencies = [
2096- "serde",
2097- ]
2098-
2099- [[package]]
2100- name = "socket2"
2101- version = "0.5.7"
2102- source = "registry+https://github.com/rust-lang/crates.io-index"
2103- checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
2104- dependencies = [
2105- "libc",
2106- "windows-sys",
2107- ]
2108-
2109- [[package]]
2110- name = "syn"
2111- version = "2.0.72"
2112- source = "registry+https://github.com/rust-lang/crates.io-index"
2113- checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
2114- dependencies = [
2115- "proc-macro2",
2116- "quote",
2117- "unicode-ident",
2118- ]
2119-
2120- [[package]]
2121- name = "tokio"
2122- version = "1.39.2"
2123- source = "registry+https://github.com/rust-lang/crates.io-index"
2124- checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1"
2125- dependencies = [
2126- "backtrace",
2127- "bytes",
2128- "libc",
2129- "mio",
2130- "parking_lot",
2131- "pin-project-lite",
2132- "signal-hook-registry",
2133- "socket2",
2134- "tokio-macros",
2135- "windows-sys",
2136- ]
2137-
2138- [[package]]
2139- name = "tokio-macros"
2140- version = "2.4.0"
2141- source = "registry+https://github.com/rust-lang/crates.io-index"
2142- checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
2143- dependencies = [
2144- "proc-macro2",
2145- "quote",
2146- "syn",
2147- ]
2148-
2149- [[package]]
2150- name = "tokio-util"
2151- version = "0.7.11"
2152- source = "registry+https://github.com/rust-lang/crates.io-index"
2153- checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
2154- dependencies = [
2155- "bytes",
2156- "futures-core",
2157- "futures-io",
2158- "futures-sink",
2159- "futures-util",
2160- "hashbrown",
2161- "pin-project-lite",
2162- "slab",
2163- "tokio",
2164- ]
2165-
2166- [[package]]
2167- name = "unicode-ident"
2168- version = "1.0.12"
2169- source = "registry+https://github.com/rust-lang/crates.io-index"
2170- checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
2171-
2172- [[package]]
2173- name = "version_check"
2174- version = "0.9.5"
2175- source = "registry+https://github.com/rust-lang/crates.io-index"
2176- checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2177-
2178- [[package]]
2179- name = "wasi"
2180- version = "0.11.0+wasi-snapshot-preview1"
2181- source = "registry+https://github.com/rust-lang/crates.io-index"
2182- checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
2183-
2184- [[package]]
2185- name = "windows-sys"
2186- version = "0.52.0"
2187- source = "registry+https://github.com/rust-lang/crates.io-index"
2188- checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2189- dependencies = [
2190- "windows-targets",
2191- ]
2192-
2193- [[package]]
2194- name = "windows-targets"
2195- version = "0.52.6"
2196- source = "registry+https://github.com/rust-lang/crates.io-index"
2197- checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
2198- dependencies = [
2199- "windows_aarch64_gnullvm",
2200- "windows_aarch64_msvc",
2201- "windows_i686_gnu",
2202- "windows_i686_gnullvm",
2203- "windows_i686_msvc",
2204- "windows_x86_64_gnu",
2205- "windows_x86_64_gnullvm",
2206- "windows_x86_64_msvc",
2207- ]
2208-
2209- [[package]]
2210- name = "windows_aarch64_gnullvm"
2211- version = "0.52.6"
2212- source = "registry+https://github.com/rust-lang/crates.io-index"
2213- checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
2214-
2215- [[package]]
2216- name = "windows_aarch64_msvc"
2217- version = "0.52.6"
2218- source = "registry+https://github.com/rust-lang/crates.io-index"
2219- checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
2220-
2221- [[package]]
2222- name = "windows_i686_gnu"
2223- version = "0.52.6"
2224- source = "registry+https://github.com/rust-lang/crates.io-index"
2225- checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
2226-
2227- [[package]]
2228- name = "windows_i686_gnullvm"
2229- version = "0.52.6"
2230- source = "registry+https://github.com/rust-lang/crates.io-index"
2231- checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
2232-
2233- [[package]]
2234- name = "windows_i686_msvc"
2235- version = "0.52.6"
2236- source = "registry+https://github.com/rust-lang/crates.io-index"
2237- checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
2238-
2239- [[package]]
2240- name = "windows_x86_64_gnu"
2241- version = "0.52.6"
2242- source = "registry+https://github.com/rust-lang/crates.io-index"
2243- checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
2244-
2245- [[package]]
2246- name = "windows_x86_64_gnullvm"
2247- version = "0.52.6"
2248- source = "registry+https://github.com/rust-lang/crates.io-index"
2249- checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
2250-
2251- [[package]]
2252- name = "windows_x86_64_msvc"
2253- version = "0.52.6"
2254- source = "registry+https://github.com/rust-lang/crates.io-index"
2255- checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
2256-
2257- [[package]]
2258- name = "zerocopy"
2259- version = "0.7.35"
2260- source = "registry+https://github.com/rust-lang/crates.io-index"
2261- checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
2262- dependencies = [
2263- "zerocopy-derive",
2264- ]
2265-
2266- [[package]]
2267- name = "zerocopy-derive"
2268- version = "0.7.35"
2269- source = "registry+https://github.com/rust-lang/crates.io-index"
2270- checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
2271- dependencies = [
2272- "proc-macro2",
2273- "quote",
2274- "syn",
2275- ]
2276 diff --git a/maitred-debug/Cargo.toml b/maitred-debug/Cargo.toml
2277deleted file mode 100644
2278index fc12a98..0000000
2279--- a/maitred-debug/Cargo.toml
2280+++ /dev/null
2281 @@ -1,20 +0,0 @@
2282- [package]
2283- name = "maitred-debug"
2284- version = "0.1.0"
2285- edition = "2021"
2286-
2287- [dependencies]
2288- async-trait = "0.1.83"
2289- clap = { version = "4.5.20", features = ["derive"] }
2290- futures = "0.3.31"
2291- maildir = "0.6.4"
2292- maitred = {path = "../maitred", features = ["full"]}
2293- serde = "1.0.213"
2294- tokio = { version = "1.41.0", features = ["full"] }
2295- toml = "0.8.19"
2296- tracing = { version = "0.1.40", features = ["log"] }
2297- tracing-subscriber = "0.3.18"
2298-
2299- [[bin]]
2300- name = "maitred-debug"
2301- path = "src/main.rs"
2302 diff --git a/maitred-debug/src/config.rs b/maitred-debug/src/config.rs
2303deleted file mode 100644
2304index 9a5ff27..0000000
2305--- a/maitred-debug/src/config.rs
2306+++ /dev/null
2307 @@ -1,34 +0,0 @@
2308- use std::path::PathBuf;
2309-
2310- #[derive(Clone, serde::Deserialize)]
2311- pub(crate) struct Account {
2312- pub address: String,
2313- }
2314-
2315- #[derive(Clone, serde::Deserialize)]
2316- pub(crate) struct Spf {
2317- pub enabled: bool,
2318- }
2319-
2320- #[derive(Clone, serde::Deserialize)]
2321- pub(crate) struct Dkim {
2322- pub enabled: bool,
2323- }
2324-
2325- #[derive(Clone, serde::Deserialize)]
2326- pub(crate) struct Tls {
2327- pub certificate: PathBuf,
2328- pub key: PathBuf,
2329- }
2330-
2331- #[derive(serde::Deserialize)]
2332- pub(crate) struct Config {
2333- pub address: String,
2334- pub level: String,
2335- pub maildir: String,
2336- pub spf: Spf,
2337- pub dkim: Dkim,
2338- pub accounts: Vec<Account>,
2339- pub tls: Option<Tls>,
2340- pub proxy_protocol: Option<bool>,
2341- }
2342 diff --git a/maitred-debug/src/main.rs b/maitred-debug/src/main.rs
2343deleted file mode 100644
2344index cd5d2bf..0000000
2345--- a/maitred-debug/src/main.rs
2346+++ /dev/null
2347 @@ -1,125 +0,0 @@
2348- use std::collections::BTreeMap;
2349- use std::fs::read_to_string;
2350- use std::path::Path;
2351- use std::str::FromStr;
2352-
2353- use clap::Parser;
2354- use maildir::Maildir;
2355- use toml::from_str;
2356- use tracing::Level;
2357-
2358- mod config;
2359-
2360- use maitred::delivery::{Delivery, DeliveryError};
2361- use maitred::mail_parser::Message;
2362- use maitred::milter::MilterFunc;
2363- use maitred::server::Server;
2364- use maitred::session::Envelope;
2365-
2366- const LONG_ABOUT: &str = r#"
2367- Maitred SMTP Demo Server
2368-
2369- NOTE: This tool only exists for illustrative purposes and should not be
2370- considered stable or suitable for a production environment.
2371- "#;
2372-
2373- /// Maitred SMTP Demo Server
2374- #[derive(Parser, Debug)]
2375- #[clap(author, version, about, long_about = LONG_ABOUT)]
2376- struct Args {
2377- /// Addresses from which to accept e-mail for
2378- #[clap(long, default_value = "maitred.toml")]
2379- config: String,
2380- }
2381-
2382- /// FSDelivery stores incoming e-mail on the file system in the Maildir format
2383- /// for each address it's configured to handle.
2384- pub struct FSDelivery {
2385- maildirs: BTreeMap<String, Maildir>,
2386- }
2387-
2388- impl FSDelivery {
2389- /// Initialize a new Maildir on the file system.
2390- pub fn new(path: &Path, addresses: &[String]) -> Result<Self, std::io::Error> {
2391- let maildirs: Result<Vec<(String, Maildir)>, std::io::Error> = addresses
2392- .iter()
2393- .map(|address| {
2394- let mbox_dir = path.join(address);
2395- let maildir: Maildir = mbox_dir.into();
2396- maildir.create_dirs()?;
2397- Ok((address.to_string(), maildir))
2398- })
2399- .collect();
2400- let maildirs = maildirs?;
2401- Ok(FSDelivery {
2402- maildirs: maildirs.into_iter().collect(),
2403- })
2404- }
2405- }
2406-
2407- #[async_trait::async_trait]
2408- impl Delivery for FSDelivery {
2409- async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError> {
2410- println!(
2411- "New SMTP Message:\n{}",
2412- String::from_utf8_lossy(message.body.raw_message())
2413- );
2414- for rcpt in message.rcpt_to.iter() {
2415- if let Some(maildir) = self.maildirs.get(&rcpt.email()) {
2416- maildir
2417- .store_new(message.body.raw_message())
2418- .map_err(|e| match e {
2419- maildir::MaildirError::Io(io_err) => DeliveryError::Io(io_err),
2420- maildir::MaildirError::Utf8(_) => unreachable!(),
2421- maildir::MaildirError::Time(e) => DeliveryError::Server(e.to_string()),
2422- })?;
2423- } else {
2424- tracing::warn!("Ignoring unknown e-mail account: {}", rcpt);
2425- }
2426- }
2427- Ok(())
2428- }
2429- }
2430-
2431- #[tokio::main]
2432- async fn main() -> Result<(), Box<dyn std::error::Error>> {
2433- let args = Args::parse();
2434- let config_str = read_to_string(Path::new(&args.config))?;
2435- let config: config::Config = from_str(&config_str)?;
2436- // Create a subscriber that logs events to the console
2437- tracing_subscriber::fmt()
2438- .compact()
2439- .with_line_number(true)
2440- .with_max_level(Level::from_str(&config.level)?)
2441- .init();
2442- let accounts = config.accounts.clone();
2443- let addresses: Vec<String> = accounts
2444- .iter()
2445- .map(|account| account.address.clone())
2446- .collect();
2447- // initialize maildirs before starting
2448- let delivery = FSDelivery::new(Path::new(&config.maildir), &addresses)?;
2449- let mut mail_server = Server::default()
2450- .address(&config.address)
2451- .with_milter(MilterFunc(|message: &Message<'static>| {
2452- let message = message.clone();
2453- async move { Ok(message.to_owned()) }
2454- }))
2455- .with_delivery(delivery)
2456- .dkim_verification(config.dkim.enabled)
2457- .spf_verification(config.spf.enabled);
2458-
2459- if let Some(tls_config) = config.tls {
2460- tracing::info!("TLS enabled");
2461- mail_server = mail_server.with_certificates(&tls_config.key, &tls_config.certificate);
2462- // session_opts = session_opts.starttls_enabled(true);
2463- }
2464-
2465- if config.proxy_protocol.is_some_and(|enabled| enabled) {
2466- mail_server = mail_server.proxy_protocol(true);
2467- };
2468-
2469- // mail_server = mail_server.with_session_opts(session_opts);
2470- mail_server.listen().await?;
2471- Ok(())
2472- }
2473 diff --git a/maitred.toml b/maitred.toml
2474deleted file mode 100644
2475index 7c133b3..0000000
2476--- a/maitred.toml
2477+++ /dev/null
2478 @@ -1,33 +0,0 @@
2479- # Path of the directory to deliver mail in the "maildir" format to
2480- maildir = "mail"
2481-
2482- # Hostname of our server
2483- hostname = "localhost:2525"
2484-
2485- # logging level
2486- level = "TRACE"
2487-
2488- # address to bind to
2489- address = "0.0.0.0:2525"
2490-
2491- # Enable HAProxy's PROXY Protocol
2492- proxy_protocol = false
2493-
2494- [tls]
2495- certificate = "cert.pem"
2496- key = "key.pem"
2497-
2498- [dkim]
2499- enabled = false
2500-
2501- [spf]
2502- enabled = false
2503-
2504- [[accounts]]
2505- address = "demo-1@example.org"
2506-
2507- [[accounts]]
2508- address = "demo-2@example.org"
2509-
2510- [[accounts]]
2511- address = "hello@example.org"
2512 diff --git a/maitred/Cargo.lock b/maitred/Cargo.lock
2513deleted file mode 100644
2514index 162e1b5..0000000
2515--- a/maitred/Cargo.lock
2516+++ /dev/null
2517 @@ -1,74 +0,0 @@
2518- # This file is automatically @generated by Cargo.
2519- # It is not intended for manual editing.
2520- version = 3
2521-
2522- [[package]]
2523- name = "maitred"
2524- version = "0.1.0"
2525- dependencies = [
2526- "smtp-proto",
2527- ]
2528-
2529- [[package]]
2530- name = "proc-macro2"
2531- version = "1.0.86"
2532- source = "registry+https://github.com/rust-lang/crates.io-index"
2533- checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
2534- dependencies = [
2535- "unicode-ident",
2536- ]
2537-
2538- [[package]]
2539- name = "quote"
2540- version = "1.0.36"
2541- source = "registry+https://github.com/rust-lang/crates.io-index"
2542- checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
2543- dependencies = [
2544- "proc-macro2",
2545- ]
2546-
2547- [[package]]
2548- name = "serde"
2549- version = "1.0.204"
2550- source = "registry+https://github.com/rust-lang/crates.io-index"
2551- checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
2552- dependencies = [
2553- "serde_derive",
2554- ]
2555-
2556- [[package]]
2557- name = "serde_derive"
2558- version = "1.0.204"
2559- source = "registry+https://github.com/rust-lang/crates.io-index"
2560- checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
2561- dependencies = [
2562- "proc-macro2",
2563- "quote",
2564- "syn",
2565- ]
2566-
2567- [[package]]
2568- name = "smtp-proto"
2569- version = "0.1.5"
2570- source = "registry+https://github.com/rust-lang/crates.io-index"
2571- checksum = "51b8ad3dd187f0d4debab02ad65405a9919d6a4f7bce25bd64a258781063a53a"
2572- dependencies = [
2573- "serde",
2574- ]
2575-
2576- [[package]]
2577- name = "syn"
2578- version = "2.0.72"
2579- source = "registry+https://github.com/rust-lang/crates.io-index"
2580- checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af"
2581- dependencies = [
2582- "proc-macro2",
2583- "quote",
2584- "unicode-ident",
2585- ]
2586-
2587- [[package]]
2588- name = "unicode-ident"
2589- version = "1.0.12"
2590- source = "registry+https://github.com/rust-lang/crates.io-index"
2591- checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
2592 diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml
2593deleted file mode 100644
2594index 74ba7e8..0000000
2595--- a/maitred/Cargo.toml
2596+++ /dev/null
2597 @@ -1,62 +0,0 @@
2598- [package]
2599- name = "maitred"
2600- version = "0.1.0"
2601- edition = "2021"
2602-
2603- [dependencies]
2604- async-trait = "0.1.83"
2605- base64 = { version = "0.22.1", optional = true }
2606- bytes = "1.8.0"
2607- crossbeam-deque = { version = "0.8.5", optional = true }
2608- email_address = "0.2.9"
2609- futures = "0.3.31"
2610- hickory-resolver = { version = "0.24.2", optional = true }
2611- mail-auth = { version = "0.5.1", features = ["ring", "rustls-pemfile"] }
2612- mail-builder = "0.3.2"
2613- mail-parser = { version = "0.9.4", features = ["serde", "serde_support"] }
2614- maildir = "0.6.4"
2615- md5 = "0.7.0"
2616- proxy-header = "0.1.2"
2617- rustls = { version = "0.23.15", optional = true }
2618- rustls-pemfile = { version = "2.2.0", optional = true }
2619- smtp-proto = { version = "0.1.5", features = ["serde", "serde_support"] }
2620- stringprep = "0.1.5"
2621- thiserror = "1.0.65"
2622- tokio = { version = "1.41.0", features = ["full"], optional = true }
2623- tokio-rustls = { version = "0.26.0", optional = true }
2624- tokio-stream = { version = "0.1.16", features = ["full"], optional = true }
2625- tokio-util = { version = "0.7.12", features = ["full"], optional = true }
2626- tracing = { version = "0.1.40", features = ["log"] }
2627- url = "2.5.2"
2628-
2629- [dependencies.lettre]
2630- version = "0.11.11"
2631- features = ["dkim", "rustls-tls", "tokio1", "tokio1-rustls-tls", "builder", "hostname", "pool", "smtp-transport"]
2632- optional = true
2633- default-features = false
2634-
2635- [dev-dependencies]
2636- port_check = "0.2.1"
2637- tracing-subscriber = "0.3.18"
2638-
2639- [features]
2640- default = []
2641- full = ["auth", "relay", "server"]
2642- auth = [
2643- "base64",
2644- "server"
2645- ]
2646- server = [
2647- "crossbeam-deque",
2648- "rustls",
2649- "rustls-pemfile",
2650- "tokio",
2651- "tokio-rustls",
2652- "tokio-stream",
2653- "tokio-util"
2654- ]
2655- relay = [
2656- "hickory-resolver",
2657- "lettre",
2658- "rustls"
2659- ]
2660 diff --git a/maitred/examples/relay.rs b/maitred/examples/relay.rs
2661deleted file mode 100644
2662index e18b287..0000000
2663--- a/maitred/examples/relay.rs
2664+++ /dev/null
2665 @@ -1,55 +0,0 @@
2666- use mail_parser::MessageParser;
2667- use maitred::relay::{Error, Relay, Sorted};
2668-
2669- const DNS_RESOLUTION_ENABLED: bool = false;
2670-
2671- const TEST_EMAIL: &str = r#"From: hello@ayllu-forge.org
2672- To: dev@localhost
2673- Cc: Fuu Bar <kevin@ayllu-forge.org>
2674- Subject: [PATCH] add delivery parameters for mail module in db crate
2675- Date: Mon, 23 Dec 2024 18:49:34 +0100
2676- Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
2677- X-Mailer: git-send-email 2.47.1
2678- MIME-Version: 1.0
2679- Content-Transfer-Encoding: 8bit
2680-
2681- From: Fuu Bar <me@example.org>
2682-
2683- ---
2684- ayllu-mail/src/delivery.rs | 12 +++++-----
2685-
2686- TRUNCATED
2687- "#;
2688-
2689- #[tokio::main]
2690- async fn main() {
2691- tracing_subscriber::fmt()
2692- .compact()
2693- .with_line_number(true)
2694- .init();
2695- maitred::crypto::init();
2696- let parser = MessageParser::new();
2697- let message = parser.parse(TEST_EMAIL).unwrap();
2698- let sorted = Sorted::from_message(&message).unwrap();
2699- let relay = Relay::builder()
2700- .port(2525)
2701- .resolve_dns(DNS_RESOLUTION_ENABLED)
2702- .build();
2703- for (domain, envelope) in sorted.0.iter() {
2704- println!("Delivering message to domain: {}", domain);
2705- match relay.send(domain, envelope, message.raw_message()).await {
2706- Ok(_) => {
2707- println!("Message delivered successfully");
2708- }
2709- Err(Error::LettreTransport(errors)) => {
2710- eprintln!("All delivery attempts failed:");
2711- for (i, attempt) in errors.iter().enumerate() {
2712- eprintln!("\tAttempt {}: {}", i, attempt);
2713- }
2714- }
2715- Err(e) => {
2716- eprintln!("Failed to send message: {}", e);
2717- }
2718- }
2719- }
2720- }
2721 diff --git a/maitred/src/auth.rs b/maitred/src/auth.rs
2722deleted file mode 100644
2723index 9094154..0000000
2724--- a/maitred/src/auth.rs
2725+++ /dev/null
2726 @@ -1,187 +0,0 @@
2727- use std::{future::Future, string::FromUtf8Error};
2728-
2729- use async_trait::async_trait;
2730- use base64::{prelude::*, DecodeError};
2731- use stringprep::{saslprep, Error as SaslPrepError};
2732-
2733- use crate::session::Response;
2734- use crate::smtp_response;
2735- use smtp_proto::Response as SmtpResponse;
2736-
2737- /// Any error that occurred during authentication.
2738- #[derive(Debug, thiserror::Error)]
2739- pub enum AuthError {
2740- #[error("Unauthorized")]
2741- Unauthorized,
2742- #[error("Input too long, maximum 255 characters")]
2743- InputTooLong,
2744- #[error("Not enough fields")]
2745- NotEnoughFields,
2746- #[error("Failed to decode authentication data: {0}")]
2747- Base64Decoding(#[from] DecodeError),
2748- #[error("Bad input: {0}")]
2749- SaslPrep(#[from] SaslPrepError),
2750- #[error("Not valid UTF8: {0}")]
2751- Utf8(#[from] FromUtf8Error),
2752- }
2753-
2754- #[allow(clippy::from_over_into)]
2755- impl Into<Response<String>> for AuthError {
2756- fn into(self) -> Response<String> {
2757- let message = self.to_string();
2758- match self {
2759- AuthError::Unauthorized => {
2760- smtp_response!(400, 0, 0, 0, message)
2761- }
2762- AuthError::InputTooLong => {
2763- smtp_response!(500, 0, 0, 0, message)
2764- }
2765- AuthError::NotEnoughFields => {
2766- smtp_response!(500, 0, 0, 0, message)
2767- }
2768- AuthError::Base64Decoding(err) => {
2769- smtp_response!(500, 0, 0, 0, err.to_string())
2770- }
2771- AuthError::SaslPrep(err) => {
2772- smtp_response!(500, 0, 0, 0, err.to_string())
2773- }
2774- AuthError::Utf8(err) => {
2775- smtp_response!(500, 0, 0, 0, err.to_string())
2776- }
2777- }
2778- }
2779- }
2780-
2781- /// Authentication trait for handling PLAIN SASL auth as defined in RFC4616
2782- #[async_trait]
2783- pub trait PlainAuth: Sync + Send {
2784- /// authenticate is passed the plaintext authcid, authzid, and passwd
2785- /// for the user. The implementer should return AuthError::Unauthorized
2786- /// if the credentials are invalid.
2787- async fn authenticate(
2788- &self,
2789- authcid: &str,
2790- authzid: &str,
2791- passwd: &str,
2792- ) -> Result<String, AuthError>;
2793- }
2794-
2795- /// Convenience function implementing PlainAuth
2796- pub struct PlainAuthFunc<F, T>(pub F)
2797- where
2798- F: Fn(&str, &str, &str) -> T + Sync + Send,
2799- T: Future<Output = Result<(), AuthError>> + Send;
2800-
2801- #[async_trait]
2802- impl<F, T> PlainAuth for PlainAuthFunc<F, T>
2803- where
2804- F: Fn(&str, &str, &str) -> T + Sync + Send,
2805- T: Future<Output = Result<(), AuthError>> + Send,
2806- {
2807- async fn authenticate(
2808- &self,
2809- authcid: &str,
2810- authzid: &str,
2811- passwd: &str,
2812- ) -> Result<String, AuthError> {
2813- let f = (self.0)(authcid, authzid, passwd);
2814- match f.await {
2815- Ok(_) => Ok(authcid.to_string()),
2816- Err(e) => Err(e),
2817- }
2818- }
2819- }
2820-
2821- /// Read a PLAIN SASL mechanism per RFC4616
2822- /// The mechanism consists of a single message, a string of [UTF-8]
2823- /// encoded [Unicode] characters, from the client to the server. The
2824- /// client presents the authorization identity (identity to act as),
2825- /// followed by a NUL (U+0000) character, followed by the authentication
2826- /// identity (identity whose password will be used), followed by a NUL
2827- /// (U+0000) character, followed by the clear-text password.
2828- #[derive(Default)]
2829- pub(crate) struct AuthData {
2830- values: [String; 3],
2831- }
2832-
2833- impl AuthData {
2834- pub fn authcid(&self) -> String {
2835- self.values[0].clone()
2836- }
2837-
2838- pub fn authzid(&self) -> String {
2839- self.values[1].clone()
2840- }
2841-
2842- pub fn passwd(&self) -> String {
2843- self.values[2].clone()
2844- }
2845- }
2846-
2847- impl TryFrom<&str> for AuthData {
2848- type Error = AuthError;
2849-
2850- fn try_from(value: &str) -> Result<Self, Self::Error> {
2851- let decoded = BASE64_STANDARD.decode(value)?;
2852- let mut n = 0;
2853- let mut raw_data: [Vec<u8>; 3] = [
2854- Vec::with_capacity(255),
2855- Vec::with_capacity(255),
2856- Vec::with_capacity(255),
2857- ];
2858- for (i, ch) in decoded.iter().enumerate() {
2859- if *ch == b'\0' {
2860- if i > 0 {
2861- n += 1;
2862- }
2863- continue;
2864- }
2865- if raw_data[n].len() + 1 > 255 {
2866- return Err(AuthError::InputTooLong);
2867- }
2868- raw_data[n].push(*ch);
2869- }
2870- if n == 0 {
2871- return Err(AuthError::NotEnoughFields);
2872- }
2873- if raw_data[2].is_empty() {
2874- // if only an athcid and passwd were specified shift the value
2875- // from authzid.
2876- raw_data[2] = raw_data[1].clone();
2877- raw_data[1] = raw_data[0].clone();
2878- }
2879- // RFC4013
2880- let sasl_authcid = String::from_utf8(raw_data[0].to_vec())?;
2881- let sasl_authcid = saslprep(&sasl_authcid)?;
2882- let sasl_authzid = String::from_utf8(raw_data[1].to_vec())?;
2883- let sasl_authzid = saslprep(&sasl_authzid)?;
2884- let sasl_passwd = String::from_utf8(raw_data[2].to_vec())?;
2885- let sasl_passwd = saslprep(&sasl_passwd)?;
2886- Ok(AuthData {
2887- values: [
2888- sasl_authcid.to_string(),
2889- sasl_authzid.to_string(),
2890- sasl_passwd.to_string(),
2891- ],
2892- })
2893- }
2894- }
2895-
2896- #[cfg(test)]
2897- mod tests {
2898-
2899- use super::*;
2900- use base64::engine::general_purpose::STANDARD;
2901-
2902- #[test]
2903- pub fn test_auth_data() {
2904- let data = AuthData::try_from(STANDARD.encode(b"\0hello\0world").as_str()).unwrap();
2905- assert!(data.authcid() == "hello");
2906- assert!(data.authzid() == "hello");
2907- assert!(data.passwd() == "world");
2908- let data = AuthData::try_from(STANDARD.encode(b"\0fuu\0bar\0baz").as_str()).unwrap();
2909- assert!(data.authcid() == "fuu");
2910- assert!(data.authzid() == "bar");
2911- assert!(data.passwd() == "baz");
2912- }
2913- }
2914 diff --git a/maitred/src/crypto.rs b/maitred/src/crypto.rs
2915deleted file mode 100644
2916index 0b8cdda..0000000
2917--- a/maitred/src/crypto.rs
2918+++ /dev/null
2919 @@ -1,8 +0,0 @@
2920- use rustls::crypto::{aws_lc_rs, CryptoProvider};
2921-
2922- /// Initialize the default crypto provider, required if using opportunistic TLS.
2923- pub fn init() {
2924- if CryptoProvider::get_default().is_none() {
2925- CryptoProvider::install_default(aws_lc_rs::default_provider()).unwrap()
2926- }
2927- }
2928 diff --git a/maitred/src/delivery.rs b/maitred/src/delivery.rs
2929deleted file mode 100644
2930index d7cd878..0000000
2931--- a/maitred/src/delivery.rs
2932+++ /dev/null
2933 @@ -1,51 +0,0 @@
2934- use std::{future::Future, io::Error as IoError};
2935-
2936- use async_trait::async_trait;
2937-
2938- use crate::session::Envelope;
2939-
2940- /// Error that occurred delivering mail
2941- #[derive(Debug, thiserror::Error)]
2942- pub enum DeliveryError {
2943- /// Indicates an unspecified error that occurred during milting.
2944- #[error("Internal Server Error: {0}")]
2945- Server(String),
2946- #[error("IO Error: {0}")]
2947- Io(#[from] IoError),
2948- }
2949-
2950- /// Delivery is the final stage of accepting an e-mail and may be invoked
2951- /// multiple times depending on the server configuration.
2952- #[async_trait]
2953- pub trait Delivery: Sync + Send {
2954- /// Persist an e-mail message in some way
2955- async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError>;
2956- }
2957-
2958- /// DeliveryFunc wraps an async closure implementing the Delivery trait.
2959- /// ```rust
2960- /// use maitred::delivery::DeliveryFunc;
2961- /// use maitred::Envelope;
2962- ///
2963- /// let delivery = DeliveryFunc(|message: &Envelope| {
2964- /// async move {
2965- /// Ok(())
2966- /// }
2967- /// });
2968- /// ```
2969- pub struct DeliveryFunc<F, T>(pub F)
2970- where
2971- F: Fn(&Envelope) -> T + Sync + Send,
2972- T: Future<Output = Result<(), DeliveryError>> + Send;
2973-
2974- #[async_trait]
2975- impl<F, T> Delivery for DeliveryFunc<F, T>
2976- where
2977- F: Fn(&Envelope) -> T + Sync + Send,
2978- T: Future<Output = Result<(), DeliveryError>> + Send,
2979- {
2980- async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError> {
2981- let f = (self.0)(message);
2982- f.await
2983- }
2984- }
2985 diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs
2986deleted file mode 100644
2987index b810ecb..0000000
2988--- a/maitred/src/expand.rs
2989+++ /dev/null
2990 @@ -1,70 +0,0 @@
2991- use std::future::Future;
2992-
2993- use async_trait::async_trait;
2994- use email_address::EmailAddress;
2995- use smtp_proto::Response as SmtpResponse;
2996-
2997- use crate::session::Response;
2998- use crate::smtp_response;
2999-
3000- /// An error encountered while expanding a mail address
3001- #[derive(Debug, thiserror::Error)]
3002- pub enum ExpansionError {
3003- /// Indicates an unspecified error that occurred during expansion.
3004- #[error("Internal Server Error: {0}")]
3005- Server(String),
3006- /// Indicates that no group exists with the specified name
3007- #[error("Group Not Found: {0}")]
3008- NotFound(String),
3009- }
3010- #[allow(clippy::from_over_into)]
3011- impl Into<Response<String>> for ExpansionError {
3012- fn into(self) -> Response<String> {
3013- match self {
3014- ExpansionError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()),
3015- ExpansionError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()),
3016- }
3017- }
3018- }
3019-
3020- /// Expands a string representing a mailing list to an array of the associated
3021- /// addresses within the list if it exists. NOTE: That this function should
3022- /// only be called with proper authentication otherwise it could be used to
3023- /// harvest e-mail addresses.
3024- #[async_trait]
3025- pub trait Expansion: Sync + Send {
3026- /// Expand the group into an array of members
3027- async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError>;
3028- }
3029-
3030- /// ExpansionFunc wraps an async closure implementing the Expansion trait
3031- /// # Example
3032- /// ```rust
3033- /// use email_address::EmailAddress;
3034- /// use maitred::expand::ExpansionFunc;
3035- ///
3036- /// let my_expn_fn = ExpansionFunc(|name: &str| {
3037- /// async move {
3038- /// Ok(vec![
3039- /// EmailAddress::new_unchecked("fuu@bar.com"),
3040- /// EmailAddress::new_unchecked("baz@qux.com")
3041- /// ])
3042- /// }
3043- /// });
3044- /// ```
3045- pub struct ExpansionFunc<F, T>(pub F)
3046- where
3047- F: Fn(&str) -> T + Sync + Send,
3048- T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send;
3049-
3050- #[async_trait]
3051- impl<F, T> Expansion for ExpansionFunc<F, T>
3052- where
3053- F: Fn(&str) -> T + Sync + Send,
3054- T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send,
3055- {
3056- async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError> {
3057- let f = (self.0)(name);
3058- f.await
3059- }
3060- }
3061 diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
3062deleted file mode 100644
3063index c842e39..0000000
3064--- a/maitred/src/lib.rs
3065+++ /dev/null
3066 @@ -1,119 +0,0 @@
3067- #![doc = include_str!("../../README.md")]
3068- //! # Example SMTP Server
3069- //! ```rust,no_run
3070- //! use maitred::auth::PlainAuthFunc;
3071- //! use maitred::delivery::{Delivery, DeliveryError, DeliveryFunc};
3072- //! use maitred::mail_parser::Message;
3073- //! use maitred::milter::MilterFunc;
3074- //! use maitred::server::{Server, ServerError};
3075- //! use maitred::session::Envelope;
3076- //!
3077- //! use tracing::Level;
3078- //!
3079- //! fn print_message(envelope: &Envelope) {
3080- //! println!("New SMTP Message:");
3081- //! println!("{:?}", envelope.body.headers());
3082- //! println!("Subject: {:?}", envelope.body.subject());
3083- //! println!(
3084- //! "{}",
3085- //! envelope.body
3086- //! .body_text(0)
3087- //! .map(|text| String::from_utf8_lossy(text.as_bytes()).to_string())
3088- //! .unwrap_or_default()
3089- //! );
3090- //! }
3091- //!
3092- //! #[tokio::main]
3093- //! async fn main() -> Result<(), ServerError> {
3094- //! // Create a subscriber that logs events to the console
3095- //! tracing_subscriber::fmt()
3096- //! .compact()
3097- //! .with_line_number(true)
3098- //! .with_max_level(Level::DEBUG)
3099- //! .init();
3100- //! // Set the subscriber as the default subscriber
3101- //! let mut mail_server = Server::default()
3102- //! .address("127.0.0.1:2525")
3103- //! .with_delivery(DeliveryFunc(|envelope: &Envelope| {
3104- //! print_message(envelope);
3105- //! async move { Ok(()) }
3106- //! }));
3107- //! mail_server.listen().await?;
3108- //! Ok(())
3109- //! }
3110- //! ```
3111-
3112- pub use email_address;
3113- pub use mail_parser;
3114- pub use smtp_proto;
3115-
3116- mod opportunistic;
3117- mod rewrite;
3118-
3119- /// SMTP Authentication
3120- #[cfg(feature = "auth")]
3121- pub mod auth;
3122- #[cfg(feature = "auth")]
3123- #[doc(inline)]
3124- pub use auth::{AuthError, PlainAuth, PlainAuthFunc};
3125-
3126- /// Low level SMTP session without network transport
3127- pub mod session;
3128- #[doc(inline)]
3129- pub use session::{
3130- Action, Envelope, Response, Session, DEFAULT_CAPABILITIES, DEFAULT_GREETING,
3131- DEFAULT_HELP_BANNER, DEFAULT_MAXIMUM_MESSAGE_SIZE,
3132- };
3133-
3134- /// Full featured tokio based TCP server for handling SMTP sessions
3135- #[cfg(feature = "server")]
3136- pub mod server;
3137- #[cfg(feature = "server")]
3138- /// Low level line oriented transport for SMTP messages
3139- pub mod transport;
3140- #[cfg(feature = "server")]
3141- mod validation;
3142- #[cfg(feature = "server")]
3143- mod worker;
3144-
3145- #[cfg(feature = "server")]
3146- #[doc(inline)]
3147- pub use server::{Server, ServerError, DEFAULT_GLOBAL_TIMEOUT_SECS, DEFAULT_LISTEN_ADDR};
3148- #[cfg(feature = "server")]
3149- #[doc(inline)]
3150- pub use transport::{Command, Transport, TransportError};
3151- /// Message Delivery
3152- #[cfg(feature = "server")]
3153- pub mod delivery;
3154- #[doc(inline)]
3155- pub use delivery::{Delivery, DeliveryError, DeliveryFunc};
3156- /// Callback for implementing SMTP command EXPN
3157- #[cfg(feature = "server")]
3158- pub mod expand;
3159- #[doc(inline)]
3160- pub use expand::{Expansion, ExpansionError, ExpansionFunc};
3161- /// Message and envelope manipulation
3162- #[cfg(feature = "server")]
3163- pub mod milter;
3164- #[doc(inline)]
3165- pub use milter::{Milter, MilterError, MilterFunc};
3166- /// Callback for implementing SMPT command VRFY
3167- #[cfg(feature = "server")]
3168- pub mod verify;
3169- #[doc(inline)]
3170- pub use verify::{Verify, VerifyError, VerifyFunc};
3171- /// DNS Resolution Helper
3172- #[cfg(feature = "relay")]
3173- pub mod relay;
3174- /// Crypto helpers
3175- #[cfg(any(feature = "server", feature="relay"))]
3176- pub mod crypto;
3177-
3178- /// Generate a single smtp_response
3179- macro_rules! smtp_response {
3180- ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
3181- Response::General(SmtpResponse::new($code, $e1, $e2, $e3, $name.to_string()))
3182- };
3183- }
3184-
3185- pub(crate) use smtp_response;
3186 diff --git a/maitred/src/milter.rs b/maitred/src/milter.rs
3187deleted file mode 100644
3188index b82e257..0000000
3189--- a/maitred/src/milter.rs
3190+++ /dev/null
3191 @@ -1,50 +0,0 @@
3192- use std::future::Future;
3193-
3194- use async_trait::async_trait;
3195- use mail_parser::Message;
3196-
3197- /// Milter error is a milter specific error
3198- #[derive(Debug, thiserror::Error)]
3199- pub enum MilterError {
3200- /// Indicates an unspecified error that occurred during milting.
3201- #[error("Internal Server Error: {0}")]
3202- Server(String),
3203- }
3204-
3205- /// A [Milter](https://en.wikipedia.org/wiki/Milter) accepts an email message
3206- /// and performs some permutation, modification, or rejection and then returns
3207- /// the message.
3208-
3209- #[async_trait]
3210- pub trait Milter: Sync + Send {
3211- async fn apply(&self, message: &Message<'static>) -> Result<Message<'static>, MilterError>;
3212- }
3213-
3214- /// MilterFunc wraps an async closure implementing the Milter trait.
3215- /// ```rust
3216- /// use mail_parser::Message;
3217- /// use maitred::milter::MilterFunc;
3218- ///
3219- /// let milter = MilterFunc(|message: &Message<'static>| {
3220- /// async move {
3221- /// // rewrite message here
3222- /// Ok(Message::default().to_owned())
3223- /// }
3224- /// });
3225- /// ```
3226- pub struct MilterFunc<F, T>(pub F)
3227- where
3228- F: Fn(&Message<'static>) -> T + Sync + Send,
3229- T: Future<Output = Result<Message<'static>, MilterError>> + Send;
3230-
3231- #[async_trait]
3232- impl<F, T> Milter for MilterFunc<F, T>
3233- where
3234- F: Fn(&Message<'static>) -> T + Sync + Send,
3235- T: Future<Output = Result<Message<'static>, MilterError>> + Send,
3236- {
3237- async fn apply(&self, message: &Message<'static>) -> Result<Message<'static>, MilterError> {
3238- let f = (self.0)(message);
3239- f.await
3240- }
3241- }
3242 diff --git a/maitred/src/opportunistic.rs b/maitred/src/opportunistic.rs
3243deleted file mode 100644
3244index 4cf40e5..0000000
3245--- a/maitred/src/opportunistic.rs
3246+++ /dev/null
3247 @@ -1,62 +0,0 @@
3248- use std::sync::Arc;
3249-
3250- use futures::SinkExt;
3251- use futures::StreamExt;
3252- use tokio::sync::Mutex;
3253- use tokio_rustls::server::TlsStream;
3254- use tokio_util::codec::Framed;
3255-
3256- use crate::session::Response;
3257- use crate::transport::{Command, Transport, TransportError};
3258-
3259- /// Connection that is either over plain text or TLS
3260- pub(crate) trait Opportunistic {
3261- async fn send(&self, message: Response<String>) -> Result<(), TransportError>;
3262- async fn next(&self) -> Option<Result<Command, TransportError>>;
3263- }
3264-
3265- /// Framed SMTP Transport over Plain Text
3266- pub(crate) struct Plain<'a, T>
3267- where
3268- T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
3269- {
3270- pub inner: Arc<Mutex<Framed<&'a mut T, Transport>>>,
3271- }
3272-
3273- impl<'a, T> Opportunistic for Plain<'a, T>
3274- where
3275- T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
3276- {
3277- async fn send(&self, message: Response<String>) -> Result<(), TransportError> {
3278- let mut inner = self.inner.lock().await;
3279- inner.send(message).await
3280- }
3281-
3282- async fn next(&self) -> Option<Result<Command, TransportError>> {
3283- let mut inner = self.inner.lock().await;
3284- inner.next().await
3285- }
3286- }
3287-
3288- /// Framed SMTP Transport over TLS
3289- pub(crate) struct Tls<'a, T>
3290- where
3291- T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
3292- {
3293- pub inner: Arc<Mutex<Framed<TlsStream<&'a mut T>, Transport>>>,
3294- }
3295-
3296- impl<'a, T> Opportunistic for Tls<'a, T>
3297- where
3298- T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
3299- {
3300- async fn send(&self, message: Response<String>) -> Result<(), TransportError> {
3301- let mut inner = self.inner.lock().await;
3302- inner.send(message).await
3303- }
3304-
3305- async fn next(&self) -> Option<Result<Command, TransportError>> {
3306- let mut inner = self.inner.lock().await;
3307- inner.next().await
3308- }
3309- }
3310 diff --git a/maitred/src/relay.rs b/maitred/src/relay.rs
3311deleted file mode 100644
3312index d52c4fa..0000000
3313--- a/maitred/src/relay.rs
3314+++ /dev/null
3315 @@ -1,290 +0,0 @@
3316- use std::collections::HashMap;
3317- use std::fmt::Display;
3318- use std::time::Duration;
3319-
3320- use hickory_resolver::error::ResolveError;
3321- use hickory_resolver::proto::rr::rdata::MX;
3322- use hickory_resolver::system_conf::read_system_conf;
3323- use hickory_resolver::TokioAsyncResolver;
3324- use lettre::address::Envelope as LettreEnvelope;
3325- use lettre::error::Error as LettreError;
3326- use lettre::transport::smtp::client::{Tls, TlsParameters};
3327- use lettre::transport::smtp::extension::ClientId;
3328- use lettre::transport::smtp::Error as LettreTransportError;
3329- use lettre::{address::AddressError, Address as LettreAddress, AsyncTransport};
3330- use lettre::{AsyncSmtpTransport, Tokio1Executor};
3331- use mail_parser::{Address, Message};
3332-
3333- const DEFAULT_SUBMISSION_PORT: u16 = 25;
3334- const DEFAULT_TIMEOUT_MS: u64 = 5000;
3335-
3336- /// Relay level errors
3337- #[derive(Debug, thiserror::Error)]
3338- pub enum Error {
3339- #[error("Message does not contain a TO field")]
3340- NoToAddress,
3341- #[error("Message does not contain a FROM field")]
3342- NoFromAddress,
3343- #[error("Cannot parse email address: {0}")]
3344- Address(#[from] AddressError),
3345- #[error("Client error: {0}")]
3346- Lettre(#[from] LettreError),
3347- #[error("Lettre transport failures")]
3348- LettreTransport(Vec<LettreTransportError>),
3349- #[error("DNS Resolution: {0}")]
3350- Resolution(#[from] ResolveError),
3351- }
3352-
3353- fn addresses(addr: Option<&Address<'_>>) -> Result<Vec<LettreAddress>, Error> {
3354- Ok(addr
3355- .map(|addr| {
3356- addr.iter().try_fold(Vec::new(), |mut accm, addr| {
3357- let address: LettreAddress = addr.address().unwrap().parse()?;
3358- accm.push(address);
3359- Ok::<Vec<LettreAddress>, Error>(accm)
3360- })
3361- })
3362- .transpose()?
3363- .unwrap_or(Vec::new()))
3364- }
3365-
3366- /// Sorted pairs of envelopes organized by domain
3367- #[derive(Clone, Default)]
3368- pub struct Sorted(pub HashMap<Hostname, LettreEnvelope>);
3369-
3370- impl Sorted {
3371- /// Return a sorted list of domains specified in the message with their
3372- /// envelope for use in the Lettre transport.
3373- pub fn from_message(message: &Message<'_>) -> Result<Self, Error> {
3374- let from = message.from().map_or(Err(Error::NoFromAddress), |from| {
3375- from.first().map_or(Err(Error::NoFromAddress), |from| {
3376- let address: LettreAddress = from.address().unwrap().parse()?;
3377- Ok(address)
3378- })
3379- })?;
3380- let unsorted = [
3381- addresses(message.to())?,
3382- addresses(message.cc())?,
3383- addresses(message.bcc())?, // TODO: Should BCC work the same?
3384- ]
3385- .concat();
3386- let sorted: HashMap<String, Vec<LettreAddress>> =
3387- unsorted.iter().fold(HashMap::new(), |mut accm, address| {
3388- if let Some(addresses) = accm.get_mut(address.domain()) {
3389- addresses.push(address.clone());
3390- } else {
3391- accm.insert(address.domain().to_string(), vec![address.clone()]);
3392- }
3393- accm
3394- });
3395- Ok(Sorted(sorted.iter().fold(
3396- HashMap::new(),
3397- |mut accm, (domain, addresses)| {
3398- accm.insert(
3399- Hostname::new(domain.as_ref()),
3400- LettreEnvelope::new(Some(from.clone()), addresses.clone()).unwrap(),
3401- );
3402- accm
3403- },
3404- )))
3405- }
3406- }
3407-
3408- /// RFC1123 Hostname
3409- /// TODO: Actually implement RFC1123
3410- #[derive(Clone, Debug, Eq, Hash, PartialEq)]
3411- pub struct Hostname(String);
3412-
3413- impl Hostname {
3414- pub fn new(hostname: &str) -> Self {
3415- Hostname(hostname.to_string())
3416- }
3417- }
3418-
3419- impl From<Hostname> for String {
3420- fn from(val: Hostname) -> Self {
3421- val.0.clone()
3422- }
3423- }
3424-
3425- impl From<&str> for Hostname {
3426- fn from(value: &str) -> Self {
3427- Hostname(value.to_string())
3428- }
3429- }
3430-
3431- impl Display for Hostname {
3432- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3433- f.write_str(self.0.as_ref())
3434- }
3435- }
3436-
3437- #[derive(Clone)]
3438- pub enum TlsConfiguration {
3439- Insecure,
3440- Opportunistic,
3441- }
3442-
3443- #[derive(Default)]
3444- pub struct RelayBuilder {
3445- hostname: Option<ClientId>,
3446- port: Option<u16>,
3447- tls: Option<TlsConfiguration>,
3448- resolve_dns: Option<bool>,
3449- }
3450-
3451- impl RelayBuilder {
3452- pub fn build(&self) -> Relay {
3453- Relay {
3454- hostname: self
3455- .hostname
3456- .as_ref()
3457- .map_or(ClientId::default(), |hostname| hostname.clone()),
3458- port: self.port.unwrap_or(DEFAULT_SUBMISSION_PORT),
3459- tls: match self.tls {
3460- Some(TlsConfiguration::Insecure) => TlsConfiguration::Insecure,
3461- Some(TlsConfiguration::Opportunistic) | None => TlsConfiguration::Opportunistic,
3462- },
3463- resolve_dns: self.resolve_dns.is_some_and(|resolve| resolve),
3464- }
3465- }
3466-
3467- pub fn insecure(mut self) -> Self {
3468- self.tls = Some(TlsConfiguration::Insecure);
3469- self
3470- }
3471-
3472- pub fn port(mut self, port: u16) -> Self {
3473- self.port = Some(port);
3474- self
3475- }
3476-
3477- pub fn resolve_dns(mut self, enabled: bool) -> Self {
3478- self.resolve_dns = Some(enabled);
3479- self
3480- }
3481- }
3482-
3483- /// Implements a message relay
3484- pub struct Relay {
3485- /// Hostname advertised in during SMTP HELO
3486- hostname: ClientId,
3487- port: u16,
3488- tls: TlsConfiguration,
3489- /// If enabled DNS resolution will be performed to lookup MX records for
3490- /// the host. If disabled then the hostname will is assumed to be literal
3491- /// and an SMTP connection is established.
3492- resolve_dns: bool,
3493- }
3494-
3495- impl Relay {
3496- pub fn builder() -> RelayBuilder {
3497- RelayBuilder::default()
3498- }
3499-
3500- fn transport(&self, hostname: Hostname) -> Result<AsyncSmtpTransport<Tokio1Executor>, Error> {
3501- let builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname.clone())
3502- .hello_name(ClientId::Domain(self.hostname.to_string()))
3503- .timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_MS)))
3504- .port(self.port)
3505- .tls(match self.tls {
3506- TlsConfiguration::Insecure => Tls::None,
3507- TlsConfiguration::Opportunistic => {
3508- Tls::Opportunistic(TlsParameters::builder(hostname.0).build_rustls().unwrap())
3509- }
3510- });
3511- let transport = builder.build();
3512- Ok(transport)
3513- }
3514-
3515- async fn resolve_mx_record(&self, domain: &str) -> Result<Vec<Hostname>, ResolveError> {
3516- tracing::info!("Looking up MX records for domain: {}", domain);
3517- let (cfg, opts) = read_system_conf()?;
3518- let resolver = TokioAsyncResolver::tokio(cfg, opts);
3519- let response = resolver.mx_lookup(domain).await?;
3520- let mut records: Vec<MX> = response.iter().cloned().collect();
3521- records.sort_by_key(|record| record.preference());
3522- records.iter().for_each(|record| {
3523- tracing::info!("Resolved record: {}", record.to_string());
3524- });
3525- Ok(records
3526- .iter()
3527- .map(|record| Hostname::new(&record.exchange().to_utf8())) // FIXME ?
3528- .collect())
3529- }
3530-
3531- /// Send a raw message
3532- pub async fn send(
3533- &self,
3534- hostname: &Hostname,
3535- envelope: &LettreEnvelope,
3536- message: &[u8],
3537- ) -> Result<(), Error> {
3538- let hostnames = if self.resolve_dns {
3539- self.resolve_mx_record(&hostname.0).await?
3540- } else {
3541- vec![hostname.clone()]
3542- };
3543- let mut failures: Vec<LettreTransportError> = Vec::new();
3544- for hostname in hostnames {
3545- tracing::info!("Attempting to deliver message to mail server: {}", hostname);
3546- let transport = self.transport(hostname.clone())?;
3547- match transport.send_raw(envelope, message).await {
3548- Ok(_) => return Ok(()),
3549- Err(e) => {
3550- tracing::warn!("Failed to relay message to mail server: {}", e);
3551- failures.push(e);
3552- continue;
3553- }
3554- }
3555- }
3556- Err(Error::LettreTransport(failures))
3557- }
3558- }
3559-
3560- #[cfg(test)]
3561- mod test {
3562-
3563- use super::*;
3564- use mail_parser::MessageParser;
3565-
3566- const TEST_EMAIL: &str = r#"From: hello@ayllu-forge.org
3567- To: dev@ayllu-dev.local
3568- Cc: Fuu Bar <me@example.org>
3569- Subject: [PATCH] add delivery parameters for mail module in db crate
3570- Date: Mon, 23 Dec 2024 18:49:34 +0100
3571- Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
3572- X-Mailer: git-send-email 2.47.1
3573- MIME-Version: 1.0
3574- Content-Transfer-Encoding: 8bit
3575-
3576- From: Fuu Bar <me@example.org>
3577-
3578- ---
3579- ayllu-mail/src/delivery.rs | 12 +++++-----
3580-
3581- TRUNCATED
3582- "#;
3583-
3584- #[test]
3585- fn sorted_parsing() {
3586- let parser = MessageParser::new();
3587- let message = parser.parse(TEST_EMAIL).unwrap();
3588- let sorted = Sorted::from_message(&message).unwrap();
3589- assert!(sorted.0.len() == 2);
3590- let d1 = sorted.0.get(&"ayllu-dev.local".into()).unwrap();
3591- let d1_from = d1.from().unwrap();
3592- assert!(d1_from.user() == "hello");
3593- assert!(d1_from.domain() == "ayllu-forge.org");
3594- assert!(d1.to().len() == 1);
3595- assert!(d1.to()[0].user() == "dev");
3596- assert!(d1.to()[0].domain() == "ayllu-dev.local");
3597- let d2 = sorted.0.get(&"example.org".into()).unwrap();
3598- let d2_from = d2.from().unwrap();
3599- assert!(d2_from.user() == "hello");
3600- assert!(d2_from.domain() == "ayllu-forge.org");
3601- assert!(d2.to().len() == 1);
3602- assert!(d2.to()[0].user() == "me");
3603- assert!(d2.to()[0].domain() == "example.org");
3604- }
3605- }
3606 diff --git a/maitred/src/rewrite.rs b/maitred/src/rewrite.rs
3607deleted file mode 100644
3608index 8f02ee0..0000000
3609--- a/maitred/src/rewrite.rs
3610+++ /dev/null
3611 @@ -1,73 +0,0 @@
3612- use mail_parser::{HeaderName, Message, MessageParser};
3613-
3614- /// Basically a hack that can modify messages expensively re-parsing them on
3615- /// each modificaiton. The mail_parser project has mentioned adding this
3616- /// functionality and perhaps this could be upstreamed.
3617- pub struct Rewrite<'a> {
3618- parser: MessageParser,
3619- raw_message: &'a mut Vec<u8>,
3620- }
3621-
3622- impl<'a> Rewrite<'a> {
3623- pub fn new(parser: Option<MessageParser>, raw_message: &'a mut Vec<u8>) -> Rewrite<'a> {
3624- Self {
3625- parser: parser.unwrap_or_default(),
3626- raw_message,
3627- }
3628- }
3629-
3630- pub fn message(&'a self) -> Message<'a> {
3631- self.parser
3632- .parse(self.raw_message.as_slice())
3633- .expect("Cannot parse message")
3634- }
3635-
3636- /// Set the header to a string value replacing it if it already exists
3637- pub fn set_header(&mut self, key: HeaderName, value: &str) {
3638- let message = self
3639- .parser
3640- .parse_headers(self.raw_message.as_slice())
3641- .expect("Cannot parse message");
3642- if let Some(header) = message.headers().iter().find(|header| header.name() == key.as_str()) {
3643- let (start, end) = (header.offset_field(), header.offset_end());
3644- self.raw_message.drain(start..end);
3645- }
3646- let header: Vec<u8> = format!("{}: {}\n", key, value.trim_end()).bytes().collect();
3647- self.raw_message.splice(0..0, header);
3648- }
3649- }
3650-
3651- #[cfg(test)]
3652- mod test {
3653- use super::*;
3654-
3655- const TEST_EMAIL: &str = r#"Date: Mon, 2 Sep 2024 00:17:18 +0200
3656- From: kevin <kevin@ayllu-dev.local>
3657- To: hello@example.org
3658- Subject: Fuu
3659- Message-ID: <ewo47gsen3mimmdzlg5v4otplgiwwyogq7avzbs26lxnir3rem@xgg5xxzon6f7>
3660- MIME-Version: 1.0
3661- Content-Type: text/plain; charset=us-ascii
3662- Content-Disposition: inline
3663-
3664- Hello World
3665- "#;
3666-
3667- #[test]
3668- fn rewrite() {
3669- let email_bytes = &mut TEST_EMAIL.as_bytes().to_vec();
3670- let mut rewrite = Rewrite::new(None, email_bytes);
3671- rewrite.set_header(HeaderName::Other("a".into()), "b");
3672- rewrite.set_header(HeaderName::Subject, "Bar");
3673- let message = rewrite.message();
3674- println!("{}", String::from_utf8_lossy(message.raw_message()));
3675- let value = message.header("a").unwrap();
3676- assert!(value.as_text().unwrap() == "b");
3677- let value = message.header("Subject").unwrap();
3678- assert!(value.as_text().unwrap() == "Bar");
3679- let message_str = String::from_utf8(message.raw_message().to_vec()).unwrap();
3680- assert!(message_str.split("\n").next().unwrap() == "Subject: Bar");
3681- assert!(message_str.split("\n").nth(1).unwrap() == "a: b");
3682- assert!(message_str.split("\n").nth(2).unwrap() == "Date: Mon, 2 Sep 2024 00:17:18 +0200");
3683- }
3684- }
3685 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
3686deleted file mode 100644
3687index e6b6192..0000000
3688--- a/maitred/src/server.rs
3689+++ /dev/null
3690 @@ -1,739 +0,0 @@
3691- use std::fs::File as StdFile;
3692- use std::io::BufReader as StdBufReader;
3693- use std::net::{IpAddr, SocketAddr};
3694- use std::path::{Path, PathBuf};
3695- use std::sync::Arc;
3696- use std::time::Duration;
3697-
3698- use crossbeam_deque::Injector;
3699- use crossbeam_deque::Stealer;
3700- use crossbeam_deque::Worker as WorkQueue;
3701- use futures::SinkExt;
3702- use futures::StreamExt;
3703- use mail_auth::Resolver;
3704- use proxy_header::{ParseConfig, ProxyHeader};
3705- use smtp_proto::Response as SmtpResponse;
3706- use tokio::net::TcpListener;
3707- use tokio::sync::mpsc::Sender;
3708- use tokio::sync::Mutex;
3709- use tokio::task::JoinHandle;
3710- use tokio::time::timeout;
3711- use tokio_rustls::{rustls, TlsAcceptor};
3712- use tokio_stream::{self as stream};
3713- use tokio_util::codec::Framed;
3714-
3715- use crate::auth::PlainAuth;
3716- use crate::delivery::Delivery;
3717- use crate::expand::Expansion;
3718- use crate::milter::Milter;
3719- use crate::opportunistic::{Opportunistic, Plain, Tls};
3720- use crate::session::{Envelope, Response, Session};
3721- use crate::transport::{Command, Transport, TransportError};
3722- use crate::validation::Validation;
3723- use crate::verify::Verify;
3724- use crate::worker::Worker;
3725-
3726- /// The default port the server will listen on if none was specified in it's
3727- /// configuration options.
3728- pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525";
3729-
3730- /// Maximum amount of time the server will wait for a command before closing
3731- /// the connection.
3732- pub const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300;
3733-
3734- /// Top level error encountered while processing a client connection, causes
3735- /// a warning to be logged but is not fatal.
3736- #[derive(Debug, thiserror::Error)]
3737- pub enum ServerError {
3738- /// An IO related error such as not being able to bind to a TCP socket
3739- #[error("Io: {0}")]
3740- Io(#[from] std::io::Error),
3741- #[error("Transport Error: {0}")]
3742- Transport(#[from] TransportError),
3743- /// Session timeout
3744- #[error("Client took too long to respond: {0}s")]
3745- Timeout(u64),
3746- #[error("Failed to configure TLS: {0}")]
3747- TlsConfiguration(#[from] rustls::Error),
3748- #[error("Proxy Protocol Error: {0}")]
3749- ProxyProtocol(#[from] proxy_header::Error),
3750- }
3751-
3752- /// Action for controlling a TCP session
3753- pub(crate) enum Action {
3754- Continue,
3755- Enqueue(Envelope),
3756- Shutdown,
3757- TlsUpgrade,
3758- }
3759-
3760- /// Server implements everything that is required to run an SMTP server by
3761- /// binding to the configured address and processing individual TCP connections
3762- /// as they are received.
3763- pub struct Server {
3764- address: String,
3765- our_hostname: String,
3766- global_timeout: Duration,
3767- pipelining: bool,
3768- milter: Option<Arc<dyn Milter>>,
3769- delivery: Option<Arc<dyn Delivery>>,
3770- n_threads: usize,
3771- shutdown_handles: Vec<Sender<bool>>,
3772- dkim_verification: bool,
3773- spf_verification: bool,
3774- list_expansion: Option<Arc<dyn Expansion>>,
3775- verification: Option<Arc<dyn Verify>>,
3776- plain_auth: Option<Arc<dyn PlainAuth>>,
3777- resolver: Option<Arc<Mutex<Resolver>>>,
3778- tls_certificates: Option<(PathBuf, PathBuf)>,
3779- proxy_protocol: bool,
3780- session: Session,
3781- }
3782-
3783- impl Default for Server {
3784- fn default() -> Self {
3785- Server {
3786- address: DEFAULT_LISTEN_ADDR.to_string(),
3787- our_hostname: String::default(),
3788- global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS),
3789- pipelining: true,
3790- milter: None,
3791- delivery: None,
3792- n_threads: std::thread::available_parallelism().unwrap().into(),
3793- shutdown_handles: vec![],
3794- dkim_verification: false,
3795- spf_verification: false,
3796- list_expansion: None,
3797- plain_auth: None,
3798- verification: None,
3799- resolver: None,
3800- tls_certificates: None,
3801- proxy_protocol: false,
3802- session: Session::default(),
3803- }
3804- }
3805- }
3806-
3807- impl Server {
3808- /// Listener address for the SMTP server to bind to listen for incoming
3809- /// connections.
3810- pub fn address(mut self, address: &str) -> Self {
3811- self.address = address.to_string();
3812- self
3813- }
3814-
3815- /// The hostname of this server
3816- pub fn our_hostname(mut self, hostname: &str) -> Self {
3817- self.our_hostname = hostname.to_string();
3818- self
3819- }
3820-
3821- /// Set the maximum amount of time the server will wait for another command
3822- /// before closing the connection. RFC states the suggested time is 5m.
3823- pub fn timeout(mut self, timeout: Duration) -> Self {
3824- self.global_timeout = timeout;
3825- self
3826- }
3827-
3828- /// If piplining is supported in the transport, typically should be yes
3829- /// but the session could explicitly disable it.
3830- pub fn pipelining(mut self, enabled: bool) -> Self {
3831- self.pipelining = enabled;
3832- self
3833- }
3834-
3835- /// Process each message with the provided milter before it is delivered
3836- pub fn with_milter<T>(mut self, milter: T) -> Self
3837- where
3838- T: Milter + 'static,
3839- {
3840- self.milter = Some(Arc::new(milter));
3841- self
3842- }
3843-
3844- /// Delivery handles the delivery of the final message
3845- pub fn with_delivery<T>(mut self, delivery: T) -> Self
3846- where
3847- T: Delivery + 'static,
3848- {
3849- self.delivery = Some(Arc::new(delivery));
3850- self
3851- }
3852-
3853- pub fn list_expansion<T>(mut self, expansion: T) -> Self
3854- where
3855- T: crate::expand::Expansion + 'static,
3856- {
3857- self.list_expansion = Some(Arc::new(expansion));
3858- self
3859- }
3860-
3861- pub fn verification<T>(mut self, verification: T) -> Self
3862- where
3863- T: crate::verify::Verify + 'static,
3864- {
3865- self.verification = Some(Arc::new(verification));
3866- self
3867- }
3868-
3869- /// Perform DKIM Verification
3870- pub fn dkim_verification(mut self, enabled: bool) -> Self {
3871- self.dkim_verification = enabled;
3872- self
3873- }
3874-
3875- /// Perform SPF Verification
3876- pub fn spf_verification(mut self, enabled: bool) -> Self {
3877- self.spf_verification = enabled;
3878- self
3879- }
3880-
3881- pub fn plain_auth<T>(mut self, plain_auth: T) -> Self
3882- where
3883- T: crate::auth::PlainAuth + 'static,
3884- {
3885- self.plain_auth = Some(Arc::new(plain_auth));
3886- self
3887- }
3888-
3889- /// TLS Certificates, implies that the server should listen for TLS
3890- /// connections and maybe support STARTTLS if configured in the Session
3891- /// options.
3892- pub fn with_certificates(mut self, private_key: &Path, certificate: &Path) -> Self {
3893- self.tls_certificates = Some((private_key.to_path_buf(), certificate.to_path_buf()));
3894- self
3895- }
3896-
3897- /// Enable support for HAProxy's
3898- /// [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
3899- pub fn proxy_protocol(mut self, enabled: bool) -> Self {
3900- self.proxy_protocol = enabled;
3901- self
3902- }
3903-
3904- async fn rustls_config(&self) -> Result<rustls::ServerConfig, ServerError> {
3905- let (private_key_path, cert_path) = self
3906- .tls_certificates
3907- .as_ref()
3908- .expect("Certificates not configured");
3909- let mut cert_contents = StdBufReader::new(StdFile::open(cert_path)?);
3910- let mut private_key_contents = StdBufReader::new(StdFile::open(private_key_path)?);
3911- let certs = rustls_pemfile::certs(&mut cert_contents).collect::<Result<Vec<_>, _>>()?;
3912- let private_key = rustls_pemfile::private_key(&mut private_key_contents)?.unwrap();
3913- Ok(rustls::ServerConfig::builder()
3914- .with_no_client_auth()
3915- .with_single_cert(certs, private_key)?)
3916- }
3917-
3918- async fn verify(&self, client_ip: IpAddr, envelope: &Envelope) -> Option<Response<String>> {
3919- if !self.spf_verification {
3920- return None;
3921- }
3922- let resolver = self.resolver.as_ref().expect("resolver not configured");
3923- let resolver = resolver.lock().await;
3924- if !Validation(resolver)
3925- .verify_spf(
3926- client_ip,
3927- &envelope.hostname.to_string(),
3928- &self.our_hostname,
3929- envelope.mail_from.as_str(),
3930- )
3931- .await
3932- {
3933- return Some(crate::session::spf_rejection());
3934- }
3935- // TODO DKIM verification here instead of worker?
3936- None
3937- }
3938-
3939- /// drive the session forward
3940- async fn next(
3941- &self,
3942- client_ip: IpAddr,
3943- conn: impl Opportunistic,
3944- session: &mut Session,
3945- ) -> Result<Action, ServerError> {
3946- match timeout(self.global_timeout, conn.next()).await {
3947- Ok(Some(Ok(Command::Requests(requests)))) => {
3948- for request in requests {
3949- let action = session.next(Some(&request));
3950- match action {
3951- crate::session::Action::Send(response) => {
3952- conn.send(response).await?;
3953- }
3954- crate::session::Action::SendMany(responses) => {
3955- for response in responses {
3956- conn.send(response).await?;
3957- }
3958- }
3959- crate::session::Action::Message {
3960- initial_response,
3961- cb,
3962- } => {
3963- conn.send(initial_response).await?;
3964- match conn.next().await {
3965- Some(Ok(Command::Payload(payload))) => match cb.call(&payload) {
3966- crate::session::Action::Send(response) => {
3967- conn.send(response).await?;
3968- }
3969- crate::session::Action::Envelope {
3970- initial_response,
3971- envelope,
3972- } => {
3973- if let Some(err_msg) =
3974- self.verify(client_ip, &envelope).await
3975- {
3976- conn.send(err_msg).await?;
3977- return Ok(Action::Shutdown);
3978- }
3979- conn.send(initial_response).await?;
3980- return Ok(Action::Enqueue(envelope));
3981- }
3982- _ => unreachable!(),
3983- },
3984- _ => unreachable!(),
3985- }
3986- }
3987- crate::session::Action::PlainAuth {
3988- authcid,
3989- authzid,
3990- password,
3991- cb,
3992- } => {
3993- let plain_auth = self
3994- .plain_auth
3995- .as_ref()
3996- .expect("authentication not available");
3997- match cb
3998- .call(plain_auth.authenticate(&authcid, &authzid, &password).await)
3999- {
4000- crate::session::Action::Send(response) => {
4001- conn.send(response).await?;
4002- }
4003- _ => unreachable!(),
4004- }
4005- }
4006- crate::session::Action::Verify { address, cb } => {
4007- let verification = self
4008- .verification
4009- .as_ref()
4010- .expect("verification not available");
4011- match cb.call(verification.verify(&address).await) {
4012- crate::session::Action::Send(response) => {
4013- conn.send(response).await?;
4014- }
4015- _ => unreachable!(),
4016- }
4017- }
4018- crate::session::Action::Expand { address, cb } => {
4019- let expansion = self
4020- .list_expansion
4021- .as_ref()
4022- .expect("expansion not available");
4023- match cb.call(expansion.expand(&address).await) {
4024- crate::session::Action::Send(response) => {
4025- conn.send(response).await?;
4026- }
4027- _ => unreachable!(),
4028- }
4029- }
4030- crate::session::Action::StartTls(response) => {
4031- // Go ahead
4032- conn.send(response).await?;
4033- return Ok(Action::TlsUpgrade);
4034- }
4035- crate::session::Action::Quit(response) => {
4036- conn.send(response).await?;
4037- return Ok(Action::Shutdown);
4038- }
4039- crate::session::Action::Envelope {
4040- initial_response: _,
4041- envelope: _,
4042- } => unreachable!(),
4043- }
4044- }
4045- Ok(Action::Continue)
4046- }
4047- Ok(Some(Ok(Command::Payload(_)))) => unreachable!(),
4048- Ok(Some(Err(err))) => {
4049- tracing::warn!("Transport Error: {}", err);
4050- let response = match err {
4051- crate::transport::TransportError::PipelineNotEnabled => {
4052- crate::smtp_response!(500, 0, 0, 0, "Pipelining is not enabled")
4053- }
4054- crate::transport::TransportError::Smtp(e) => {
4055- crate::session::smtp_error_to_response(e)
4056- }
4057- // IO Errors considered fatal for the entire session
4058- crate::transport::TransportError::Io(e) => return Err(ServerError::Io(e)),
4059- };
4060- conn.send(response).await?;
4061- Ok(Action::Continue)
4062- }
4063- Ok(None) => Ok(Action::Shutdown),
4064- Err(elapsed) => {
4065- tracing::warn!("Client timeout: {}", elapsed);
4066- conn.send(crate::session::timeout(&elapsed.to_string()))
4067- .await?;
4068- Err(ServerError::Timeout(self.global_timeout.as_secs()))
4069- }
4070- }
4071- }
4072-
4073- async fn serve_plain<T>(
4074- &self,
4075- stream: &mut T,
4076- msg_queue: Arc<Injector<Envelope>>,
4077- remote_addr: SocketAddr,
4078- ) -> Result<(), ServerError>
4079- where
4080- T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
4081- {
4082- let mut session = self
4083- .session
4084- .clone()
4085- .our_hostname(&self.our_hostname)
4086- .starttls(self.tls_certificates.is_some())
4087- .vrfy_enabled(self.verification.is_some())
4088- .expn_enabled(self.list_expansion.is_some());
4089-
4090- let mut framed = Framed::new(
4091- &mut *stream,
4092- Transport::default().pipelining(self.pipelining),
4093- );
4094-
4095- // initialize the connection with a greeting
4096- match session.next(None) {
4097- crate::session::Action::Send(response) => {
4098- framed.send(response).await?;
4099- }
4100- _ => unreachable!(),
4101- }
4102-
4103- let framed = Arc::new(Mutex::new(framed));
4104-
4105- loop {
4106- match self
4107- .next(
4108- remote_addr.ip(),
4109- Plain {
4110- inner: framed.clone(),
4111- },
4112- &mut session,
4113- )
4114- .await?
4115- {
4116- Action::Continue => {}
4117- Action::Enqueue(envelope) => {
4118- msg_queue.push(envelope);
4119- }
4120- Action::Shutdown => return Ok(()),
4121- Action::TlsUpgrade => {
4122- let acceptor = TlsAcceptor::from(Arc::new(self.rustls_config().await?));
4123- let tls_stream = acceptor.accept(&mut *stream).await?;
4124- let tls_framed =
4125- Framed::new(tls_stream, Transport::default().pipelining(self.pipelining));
4126- let tls_framed = Arc::new(Mutex::new(tls_framed));
4127- // Per the RFC after TLS is established the session is
4128- // reset.
4129- let mut session = session.clone().tls_active(true);
4130- loop {
4131- match self
4132- .next(
4133- remote_addr.ip(),
4134- Tls {
4135- inner: tls_framed.clone(),
4136- },
4137- &mut session,
4138- )
4139- .await?
4140- {
4141- Action::Continue => {}
4142- Action::Enqueue(envelope) => {
4143- msg_queue.push(envelope);
4144- }
4145- Action::Shutdown => return Ok(()),
4146- Action::TlsUpgrade => unreachable!(),
4147- }
4148- }
4149- }
4150- }
4151- }
4152- }
4153-
4154- async fn spawn_workers(&mut self, global_queue: Arc<Injector<Envelope>>) {
4155- let local_queues: Vec<WorkQueue<Envelope>> = (0..self.n_threads)
4156- .map(|_| WorkQueue::<Envelope>::new_fifo())
4157- .collect();
4158- let stealers: Vec<Stealer<Envelope>> = local_queues
4159- .iter()
4160- .map(|local_queue| local_queue.stealer())
4161- .collect();
4162- let handles: Vec<JoinHandle<_>> = local_queues
4163- .into_iter()
4164- .map(|local_queue| {
4165- let (tx, shutdown_rx) = tokio::sync::mpsc::channel::<bool>(1);
4166- self.shutdown_handles.push(tx);
4167- let global_queue = global_queue.clone();
4168- let stealers = stealers.clone();
4169- let milter = self.milter.clone();
4170- let delivery = self.delivery.clone();
4171- let resolver = self.resolver.clone();
4172- let dkim_verification = self.dkim_verification;
4173- tokio::task::spawn(async move {
4174- let mut worker = Worker {
4175- milter,
4176- delivery,
4177- global_queue,
4178- stealers,
4179- local_queue: Arc::new(Mutex::new(local_queue)),
4180- shutdown_rx,
4181- resolver,
4182- dkim_verification,
4183- };
4184- worker.process().await
4185- })
4186- })
4187- .collect();
4188-
4189- // Log a message anytime a worker stops for any reason
4190- tokio::spawn(async move {
4191- stream::iter(handles)
4192- .for_each(|handle| async move {
4193- let worker_result = handle.await.unwrap();
4194- if let Err(err) = worker_result {
4195- tracing::warn!("Worker shutdown with error: {}", err);
4196- } else {
4197- tracing::info!("Worker shutdown normally");
4198- }
4199- })
4200- .await;
4201- });
4202- }
4203-
4204- pub async fn listen(&mut self) -> Result<(), ServerError> {
4205- let listener = TcpListener::bind(&self.address).await?;
4206- tracing::info!("Mail server listening @ {}", self.address);
4207- self.resolver = if self.spf_verification || self.dkim_verification {
4208- Some(Arc::new(Mutex::new(Resolver::new_system_conf().unwrap())))
4209- } else {
4210- None
4211- };
4212- let global_queue = Arc::new(Injector::<Envelope>::new());
4213- self.spawn_workers(global_queue.clone()).await;
4214- loop {
4215- let (mut socket, addr) = listener.accept().await.unwrap();
4216- let local_addr = socket.local_addr()?;
4217- tracing::info!("Accepted connection on: {:?} from: {:?}", local_addr, addr);
4218- // pass the proxied address if proxy protocol is enabled
4219- let addr = if self.proxy_protocol {
4220- let mut buf: [u8; 512] = [0; 512];
4221- socket.peek(&mut buf).await?;
4222- let (header, len) = ProxyHeader::parse(&buf, ParseConfig::default())?;
4223- tracing::info!("Parsed proxy protocol header: {:?} bytes={}", header, len);
4224- if let Some(proxied) = header.proxied_address() {
4225- // discard the proxy header
4226- let mut buf = vec![0; len];
4227- socket.readable().await?;
4228- socket.try_read(&mut buf)?;
4229- proxied.source
4230- // socket.proxied.source
4231- } else {
4232- tracing::error!("Failed to parse proxied address");
4233- addr
4234- }
4235- } else {
4236- addr
4237- };
4238- match self
4239- .serve_plain(&mut socket, global_queue.clone(), addr)
4240- .await
4241- {
4242- Ok(_) => {
4243- tracing::info!("Client connection finished normally");
4244- }
4245- Err(err) => {
4246- tracing::warn!("Client encountered an error: {:?}", err);
4247- }
4248- }
4249- }
4250- }
4251- }
4252-
4253- #[cfg(test)]
4254- mod test {
4255-
4256- use crate::DeliveryFunc;
4257-
4258- use super::*;
4259-
4260- use std::io;
4261- use std::net::{Ipv4Addr, SocketAddrV4};
4262- use std::pin::Pin;
4263- use std::task::{Context, Poll};
4264-
4265- use lettre::{AsyncSmtpTransport, AsyncTransport, Message as LettreMessage, Tokio1Executor};
4266- use port_check::free_local_ipv4_port;
4267- use tokio::io::{AsyncRead, AsyncWrite};
4268- use tokio::sync::mpsc::channel;
4269-
4270- /// Fake TCP stream for testing purposes with "framed" line oriented
4271- /// requests to feed to the session processor.
4272- #[derive(Default)]
4273- struct FakeStream {
4274- buffer: Vec<Vec<u8>>,
4275- chunk: usize,
4276- }
4277-
4278- impl AsyncRead for FakeStream {
4279- fn poll_read(
4280- self: Pin<&mut Self>,
4281- _cx: &mut Context<'_>,
4282- buf: &mut tokio::io::ReadBuf<'_>,
4283- ) -> Poll<io::Result<()>> {
4284- let inner = self.get_mut();
4285- let index = inner.chunk;
4286- if let Some(chunk) = inner.buffer.get(index) {
4287- inner.chunk = index + 1;
4288- println!("Client wrote: {:?}", String::from_utf8_lossy(chunk));
4289- buf.put_slice(chunk.as_slice());
4290- std::task::Poll::Ready(Ok(()))
4291- } else {
4292- Poll::Ready(Ok(()))
4293- }
4294- }
4295- }
4296-
4297- impl AsyncWrite for FakeStream {
4298- fn poll_write(
4299- self: Pin<&mut Self>,
4300- _cx: &mut Context<'_>,
4301- buf: &[u8],
4302- ) -> Poll<Result<usize, io::Error>> {
4303- println!("Server responded: {:?}", String::from_utf8_lossy(buf));
4304- Poll::Ready(Ok(buf.len()))
4305- }
4306-
4307- fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
4308- Poll::Ready(Ok(()))
4309- }
4310-
4311- fn poll_shutdown(
4312- self: Pin<&mut Self>,
4313- _cx: &mut Context<'_>,
4314- ) -> Poll<Result<(), io::Error>> {
4315- todo!()
4316- }
4317- }
4318-
4319- #[tokio::test]
4320- async fn test_server() {
4321- let mut stream = FakeStream {
4322- buffer: vec![
4323- "HELO example.org\r\n".into(),
4324- "MAIL FROM: <fuu@bar.com>\r\n".into(),
4325- "RCPT TO: <baz@qux.com>\r\n".into(),
4326- "DATA\r\n".into(),
4327- "Subject: Hello World\r\n.\r\n".into(),
4328- "QUIT\r\n".into(),
4329- ],
4330- ..Default::default()
4331- };
4332- let server = Server::default();
4333- // turn off all extended capabilities
4334- let global_queue = Arc::new(Injector::<Envelope>::new());
4335- server
4336- .serve_plain(
4337- &mut stream,
4338- global_queue.clone(),
4339- SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)),
4340- )
4341- .await
4342- .unwrap();
4343- let packet = global_queue.steal().success().unwrap();
4344- assert!(packet.mail_from.email() == "fuu@bar.com");
4345- assert!(packet
4346- .rcpt_to
4347- .first()
4348- .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"));
4349- }
4350-
4351- #[tokio::test]
4352- async fn test_server_pipelined() {
4353- let mut stream = FakeStream {
4354- buffer: vec![
4355- "HELO example.org\r\n".into(),
4356- "MAIL FROM: <fuu@bar.com>\r\n".into(),
4357- "RCPT TO: <baz@qux.com>\r\n".into(),
4358- "DATA\r\n".into(),
4359- "Subject: Hello World\r\n.\r\n".into(),
4360- "QUIT\r\n".into(),
4361- ],
4362- ..Default::default()
4363- };
4364- let server = Server::default();
4365- let global_queue = Arc::new(Injector::<Envelope>::new());
4366- server
4367- .serve_plain(
4368- &mut stream,
4369- global_queue.clone(),
4370- SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)),
4371- )
4372- .await
4373- .unwrap();
4374- let packet = global_queue.steal().success().unwrap();
4375- assert!(packet.mail_from.email() == "fuu@bar.com");
4376- assert!(packet
4377- .rcpt_to
4378- .first()
4379- .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"));
4380- }
4381-
4382- #[tokio::test]
4383- async fn server_is_send() {
4384- let server = Server::default();
4385- tokio::task::spawn(async {
4386- assert!(server.address("0.0.0.0:0").listen().await.is_err());
4387- });
4388- }
4389-
4390- #[tokio::test]
4391- async fn server_lettre_client() {
4392- let test_port = free_local_ipv4_port().unwrap();
4393- let (tx, mut rx) = channel::<bool>(1);
4394- tokio::task::spawn(async move {
4395- let mut server = Server::default()
4396- .address(&format!("127.0.0.1:{}", test_port))
4397- .with_delivery(DeliveryFunc(move |envelope: &Envelope| {
4398- let tx_clone = tx.clone();
4399- let matches = envelope
4400- .body
4401- .body_text(0)
4402- .is_some_and(|body| body.contains("Hello World!"));
4403- async move {
4404- tx_clone.send(matches).await.unwrap();
4405- Ok(())
4406- }
4407- }));
4408- server.listen().await.unwrap();
4409- });
4410- let transport: AsyncSmtpTransport<Tokio1Executor> =
4411- AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
4412- .port(test_port)
4413- .build();
4414- let message = LettreMessage::builder()
4415- .to("hello@example.org".parse().unwrap())
4416- .from("fuu@example.org".parse().unwrap())
4417- .cc("bar@example.org".parse().unwrap())
4418- .body(String::from("Hello World!\n"))
4419- .unwrap();
4420- // FIXME: Need synchronization in the server to tell us when it's
4421- // accepting connections.
4422- tokio::time::sleep(Duration::from_millis(500)).await;
4423- // BUG: Either the client doesn't respect batching or the server doesn't
4424- // implement it correctly (probably the latter).
4425- // assert!(transport.test_connection().await.is_ok_and(|ready| ready));
4426- transport.send(message).await.unwrap();
4427- assert!(rx.recv().await.is_some_and(|matches| matches));
4428- }
4429- }
4430 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
4431deleted file mode 100644
4432index 280a22f..0000000
4433--- a/maitred/src/session.rs
4434+++ /dev/null
4435 @@ -1,1193 +0,0 @@
4436- use std::fmt::Display;
4437- use std::str::FromStr;
4438- use std::sync::{Arc, Mutex};
4439-
4440- use email_address::EmailAddress;
4441-
4442- use mail_parser::{Message, MessageParser};
4443- use smtp_proto::{EhloResponse, Request, Response as SmtpResponse};
4444- use url::Host;
4445-
4446- use crate::auth::{AuthData, AuthError};
4447- use crate::expand::ExpansionError;
4448- use crate::smtp_response;
4449- use crate::verify::VerifyError;
4450-
4451- /// Default help banner returned from a HELP command without any parameters
4452- pub const DEFAULT_HELP_BANNER: &str = r#"
4453- Maitred ESMTP Server:
4454- see https://ayllu-forge.org/ayllu/maitred for more information.
4455- "#;
4456-
4457- /// Maximum message size the server will accept.
4458- pub const DEFAULT_MAXIMUM_MESSAGE_SIZE: u64 = 5_000_000;
4459-
4460- /// Default greeting returned by the server upon initial connection.
4461- pub const DEFAULT_GREETING: &str = "Maitred ESMTP Server";
4462-
4463- // TODO:
4464- // 250-8BITMIME
4465- // 250-DSN
4466- // 250-SMTPUTF8
4467-
4468- /// Default SMTP capabilities advertised by the server.
4469- pub const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE
4470- | smtp_proto::EXT_ENHANCED_STATUS_CODES
4471- | smtp_proto::EXT_PIPELINING
4472- | smtp_proto::EXT_8BIT_MIME;
4473-
4474- /// Wrapper around an SMTP message to send to the client.
4475- #[derive(Debug, Clone)]
4476- pub enum Response<T>
4477- where
4478- T: Display,
4479- {
4480- General(SmtpResponse<T>),
4481- Ehlo(EhloResponse<T>),
4482- }
4483-
4484- impl Response<String> {
4485- pub fn is_fatal(&self) -> bool {
4486- match self {
4487- Response::General(resp) => resp.code >= 500,
4488- Response::Ehlo(_) => false,
4489- }
4490- }
4491- }
4492-
4493- impl<T> PartialEq for Response<T>
4494- where
4495- T: Display,
4496- {
4497- fn eq(&self, other: &Self) -> bool {
4498- match self {
4499- Response::General(req) => match other {
4500- Response::General(other) => req.to_string() == other.to_string(),
4501- Response::Ehlo(_) => false,
4502- },
4503- Response::Ehlo(req) => match other {
4504- Response::General(_) => false,
4505- Response::Ehlo(other) => {
4506- // FIXME
4507- req.capabilities == other.capabilities
4508- && req.hostname.to_string() == other.hostname.to_string()
4509- && req.deliver_by == other.deliver_by
4510- && req.size == other.size
4511- && req.auth_mechanisms == other.auth_mechanisms
4512- && req.future_release_datetime.eq(&req.future_release_datetime)
4513- && req.future_release_interval.eq(&req.future_release_interval)
4514- }
4515- },
4516- }
4517- }
4518- }
4519-
4520- impl<T> Eq for Response<T> where T: Display {}
4521-
4522- #[derive(Clone, Default)]
4523- struct Capabilities {
4524- mode: Option<Mode>,
4525- capabilities: u32,
4526- }
4527-
4528- impl Capabilities {
4529- pub fn enabled(&self, capability: u32) -> bool {
4530- self.mode
4531- .as_ref()
4532- .is_some_and(|mode| matches!(mode, Mode::Extended))
4533- && self.capabilities & capability != 0
4534- }
4535- }
4536-
4537- pub struct SetMessage {
4538- inner: Arc<Mutex<Inner>>,
4539- capabilities: Capabilities,
4540- }
4541-
4542- impl SetMessage {
4543- /// checks if 8BITMIME is supported
4544- fn check_body(&self, body: &[u8]) -> Result<(), Response<String>> {
4545- if !self.capabilities.enabled(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() {
4546- return Err(smtp_response!(
4547- 500,
4548- 0,
4549- 0,
4550- 0,
4551- "Non ASCII characters found in message body"
4552- ));
4553- }
4554- Ok(())
4555- }
4556- /// Called each time a message is ready for processing, will do spf
4557- /// validation if it is configured.
4558- fn accept_payload(&self, inner: &Inner, payload: &[u8]) -> Action {
4559- if inner.rcpt_to.is_none() {
4560- return Action::Send(smtp_response!(500, 0, 0, 0, "RCPT TO is missing"));
4561- }
4562- if inner.hostname.is_none() {
4563- return Action::Send(smtp_response!(500, 0, 0, 0, "Hostname is missing"));
4564- }
4565- // let copied = payload.to_vec();
4566- if let Err(response) = self.check_body(payload) {
4567- return Action::Send(response);
4568- };
4569- let parser = MessageParser::new();
4570- match parser.parse(payload) {
4571- Some(message) => Action::Envelope {
4572- initial_response: smtp_response!(250, 0, 0, 0, "OK"),
4573- envelope: Envelope {
4574- body: message.into_owned(),
4575- // FIXME
4576- mail_from: inner.mail_from.clone().unwrap(),
4577- rcpt_to: inner.rcpt_to.clone().unwrap(),
4578- hostname: inner.hostname.clone().unwrap(),
4579- },
4580- },
4581- None => Action::Send(smtp_response!(500, 0, 0, 0, "Cannot parse message payload")),
4582- }
4583- }
4584-
4585- pub fn call(&self, message: &[u8]) -> Action {
4586- let inner = self.inner.lock().unwrap();
4587- self.accept_payload(&inner, message)
4588- }
4589- }
4590-
4591- pub struct PlainAuth(Arc<Mutex<Inner>>);
4592-
4593- impl PlainAuth {
4594- pub fn call(&self, auth_response: Result<String, AuthError>) -> Action {
4595- match auth_response {
4596- Ok(authcid) => {
4597- tracing::info!("Successfully Authenticated");
4598- let mut inner = self.0.lock().unwrap();
4599- inner.authenticated_id = Some(authcid.clone());
4600- Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
4601- }
4602- Err(e) => Action::Send(e.into()),
4603- }
4604- }
4605- }
4606-
4607- pub struct Verify;
4608-
4609- impl Verify {
4610- pub fn call(&self, verify_response: Result<(), VerifyError>) -> Action {
4611- match verify_response {
4612- Ok(_) => Action::Send(smtp_response!(200, 0, 0, 0, "OK")),
4613- Err(e) => Action::Send(e.into()),
4614- }
4615- }
4616- }
4617-
4618- pub struct Expand;
4619-
4620- impl Expand {
4621- pub fn call(&self, addresses: Result<Vec<EmailAddress>, ExpansionError>) -> Action {
4622- match addresses {
4623- Ok(addresses) => {
4624- let mut responses = vec![smtp_response!(250, 0, 0, 0, "OK")];
4625- responses.extend(
4626- addresses
4627- .iter()
4628- .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())),
4629- );
4630- Action::SendMany(responses)
4631- }
4632- Err(e) => Action::Send(e.into()),
4633- }
4634- }
4635- }
4636-
4637- /// An Envelope containing an e-mail message created from the session.
4638- #[derive(Clone, Debug)]
4639- pub struct Envelope {
4640- pub body: Message<'static>,
4641- pub mail_from: EmailAddress,
4642- pub rcpt_to: Vec<EmailAddress>,
4643- pub hostname: Host,
4644- }
4645-
4646- /// Action for the server implementor to take probably asynchronously.
4647- pub enum Action {
4648- Send(Response<String>),
4649- SendMany(Vec<Response<String>>),
4650- Message {
4651- initial_response: Response<String>,
4652- cb: SetMessage,
4653- },
4654- Envelope {
4655- initial_response: Response<String>,
4656- envelope: Envelope,
4657- },
4658- PlainAuth {
4659- authcid: String,
4660- authzid: String,
4661- password: String,
4662- cb: PlainAuth,
4663- },
4664- Verify {
4665- address: EmailAddress,
4666- cb: Verify,
4667- },
4668- Expand {
4669- address: String,
4670- cb: Expand,
4671- },
4672- StartTls(Response<String>),
4673- Quit(Response<String>),
4674- }
4675-
4676- impl Display for Action {
4677- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4678- match self {
4679- Action::Send(response) => match response {
4680- Response::General(response) => {
4681- f.write_fmt(format_args!("Send:{}", &response.to_string()))
4682- }
4683- Response::Ehlo(ehlo_response) => {
4684- f.write_fmt(format_args!("Send:{:?}", ehlo_response))
4685- }
4686- },
4687- Action::SendMany(vec) => {
4688- f.write_str("Send Many:\n")?;
4689- vec.iter().for_each(|message| match message {
4690- Response::General(response) => f
4691- .write_fmt(format_args!("{}\n", &response.to_string()))
4692- .unwrap(),
4693- Response::Ehlo(_ehlo_response) => unreachable!(),
4694- });
4695- Ok(())
4696- }
4697- Action::Message {
4698- initial_response,
4699- cb: _,
4700- } => match initial_response {
4701- Response::General(response) => f.write_fmt(format_args!("Message:\n{}", response)),
4702- Response::Ehlo(_ehlo_response) => unreachable!(),
4703- },
4704- Action::Envelope {
4705- initial_response,
4706- envelope,
4707- } => f.write_fmt(format_args!(
4708- "Envelope: {:?}, {}",
4709- initial_response, envelope.mail_from
4710- )),
4711- Action::PlainAuth {
4712- authcid,
4713- authzid,
4714- password: _,
4715- cb: _,
4716- } => f.write_fmt(format_args!("Plain Auth: {} {}", authcid, authzid)),
4717- Action::Verify { address, cb: _ } => f.write_fmt(format_args!("Verify: {}", address)),
4718- Action::Expand { address, cb: _ } => f.write_fmt(format_args!("Expand: {}", address)),
4719- Action::StartTls(response) => match response {
4720- Response::General(response) => f.write_str(&response.to_string()),
4721- Response::Ehlo(_ehlo_response) => unreachable!(),
4722- },
4723- Action::Quit(response) => match response {
4724- Response::General(response) => {
4725- f.write_fmt(format_args!("Quit: {}", &response.to_string()))
4726- }
4727- Response::Ehlo(_ehlo_response) => unreachable!(),
4728- },
4729- }
4730- }
4731- }
4732-
4733- /// If the session was started with HELO or ELHO.
4734- #[derive(Clone)]
4735- enum Mode {
4736- Legacy,
4737- Extended,
4738- }
4739-
4740- /// Sent when the connection exceeds the maximum configured timeout
4741- pub fn timeout(message: &str) -> Response<String> {
4742- smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message))
4743- }
4744-
4745- pub fn tls_already_active() -> Response<String> {
4746- smtp_response!(400, 0, 0, 0, "TLS is already active")
4747- }
4748-
4749- pub fn spf_rejection() -> Response<String> {
4750- smtp_response!(500, 0, 0, 0, "SPF Verification Failed")
4751- }
4752-
4753- pub fn smtp_error_to_response(e: smtp_proto::Error) -> Response<String> {
4754- match e {
4755- smtp_proto::Error::NeedsMoreData { bytes_left: _ } => {
4756- // TODO
4757- smtp_response!(500, 0, 0, 0, e.to_string())
4758- }
4759- smtp_proto::Error::UnknownCommand => {
4760- smtp_response!(500, 5, 5, 1, "Invalid Command")
4761- }
4762- smtp_proto::Error::InvalidSenderAddress => {
4763- smtp_response!(501, 5, 1, 8, e.to_string())
4764- }
4765- smtp_proto::Error::InvalidRecipientAddress => {
4766- smtp_response!(501, 5, 1, 3, e.to_string())
4767- }
4768- smtp_proto::Error::SyntaxError { syntax: _ } => {
4769- smtp_response!(501, 5, 5, 2, e.to_string())
4770- }
4771- smtp_proto::Error::InvalidParameter { param: _ } => {
4772- // TODO
4773- smtp_response!(500, 0, 0, 0, e.to_string())
4774- }
4775- smtp_proto::Error::UnsupportedParameter { param: _ } => {
4776- // TODO
4777- smtp_response!(500, 0, 0, 0, e.to_string())
4778- }
4779- smtp_proto::Error::ResponseTooLong => {
4780- // TODO
4781- smtp_response!(500, 0, 0, 0, e.to_string())
4782- }
4783- smtp_proto::Error::InvalidResponse { code: _ } => {
4784- // TODO
4785- smtp_response!(500, 0, 0, 0, e.to_string())
4786- }
4787- }
4788- }
4789-
4790- /// Extract a host from HELO/EHLO per RFC5321 4.1.3
4791- fn parse_host(host: &str) -> String {
4792- // confusingly the url library determines if an address is IPv6 by checking
4793- // for [ ] but SMTP uses "tags" to determine this.
4794- let n_periods = host
4795- .chars()
4796- .fold(0, |accm, c| if c == '.' { accm + 1 } else { accm });
4797- if n_periods == 3 {
4798- host.trim_start_matches("[")
4799- .trim_end_matches("]")
4800- .to_string()
4801- } else if host.contains("IPv6:") {
4802- format!("[{}]", host.replace("IPv6:", "").trim())
4803- } else {
4804- host.to_string()
4805- }
4806- }
4807-
4808- /// session runtime flags
4809- #[derive(Clone, Default)]
4810- struct Flags {
4811- authentication: bool,
4812- starttls: bool,
4813- vrfy: bool,
4814- expn: bool,
4815- }
4816-
4817- #[derive(Clone, Default)]
4818- struct Inner {
4819- /// mailto address
4820- mail_from: Option<EmailAddress>,
4821- /// rcpt address
4822- rcpt_to: Option<Vec<EmailAddress>>,
4823- /// hostname per HELO
4824- hostname: Option<Host>,
4825-
4826- initialized: Option<Mode>,
4827- // previously ran commands
4828- // TODO pipeline still partially broken
4829- history: Vec<Request<String>>,
4830- spf_verified_host: Option<String>,
4831- authenticated_id: Option<String>,
4832- }
4833-
4834- impl Inner {
4835- /// Reset the connection to it's default state but after a HELO/ELHO has
4836- /// been issued successfully.
4837- pub fn reset(&mut self) {
4838- self.mail_from = None;
4839- self.rcpt_to = None;
4840- // self.hostname = None;
4841- self.initialized = None;
4842- self.history = Vec::new();
4843- self.spf_verified_host = None;
4844- }
4845- }
4846-
4847- /// State machine that corresponds to a single SMTP session, calls to next
4848- /// return actions that the caller is expected to implement in a transport.
4849- #[derive(Clone)]
4850- pub struct Session {
4851- // /// mailto address
4852- // mail_from: Option<EmailAddress>,
4853- // /// rcpt address
4854- // rcpt_to: Option<Vec<EmailAddress>>,
4855- // /// hostname per HELO
4856- // hostname: Option<Host>,
4857-
4858- // initialized: Option<Mode>,
4859- // // previously ran commands
4860- // // TODO pipeline still partially broken
4861- // history: Vec<Request<String>>,
4862- inner: Arc<Mutex<Inner>>,
4863-
4864- // session opts
4865- our_hostname: Option<String>, // required
4866- maximum_size: u64,
4867- capabilities: u32,
4868- help_banner: String,
4869- greeting: String,
4870- tls_active: bool,
4871-
4872- flags: Flags,
4873- }
4874-
4875- impl Default for Session {
4876- fn default() -> Self {
4877- Session {
4878- inner: Arc::new(Mutex::new(Inner::default())),
4879- our_hostname: None,
4880- maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE,
4881- capabilities: DEFAULT_CAPABILITIES,
4882- help_banner: DEFAULT_HELP_BANNER.to_string(),
4883- greeting: DEFAULT_GREETING.to_string(),
4884- tls_active: false,
4885- flags: Flags::default(),
4886- }
4887- }
4888- }
4889-
4890- impl Session {
4891- pub fn our_hostname(mut self, hostname: &str) -> Self {
4892- self.our_hostname = Some(hostname.to_string());
4893- self
4894- }
4895-
4896- pub fn maximum_size(mut self, maximum_size: u64) -> Self {
4897- self.maximum_size = maximum_size;
4898- self
4899- }
4900-
4901- pub fn capabilities(mut self, capabilities: u32) -> Self {
4902- self.capabilities = capabilities;
4903- self
4904- }
4905-
4906- pub fn help_banner(mut self, help_banner: &str) -> Self {
4907- self.help_banner = help_banner.to_string();
4908- self
4909- }
4910-
4911- pub fn greeting_banner(mut self, greeting: &str) -> Self {
4912- self.greeting = greeting.to_string();
4913- self
4914- }
4915-
4916- pub fn authentication(mut self, enabled: bool) -> Self {
4917- self.flags.authentication = enabled;
4918- self.capabilities |= smtp_proto::EXT_AUTH;
4919- self
4920- }
4921-
4922- pub fn starttls(mut self, enabled: bool) -> Self {
4923- self.flags.starttls = enabled;
4924- self.capabilities |= smtp_proto::EXT_START_TLS;
4925- self
4926- }
4927-
4928- pub fn vrfy_enabled(mut self, enabled: bool) -> Self {
4929- self.flags.vrfy = enabled;
4930- self
4931- }
4932-
4933- pub fn expn_enabled(mut self, enabled: bool) -> Self {
4934- self.flags.expn = enabled;
4935- self
4936- }
4937-
4938- pub fn tls_active(mut self, active: bool) -> Self {
4939- self.tls_active = active;
4940- self
4941- }
4942-
4943- /// A greeting must be sent at the start of an SMTP connection when it is
4944- /// first initialized.
4945- /// FIXME
4946- pub fn greeting(&self) -> Response<String> {
4947- smtp_response!(
4948- 220,
4949- 2,
4950- 0,
4951- 0,
4952- format!(
4953- "{} {}",
4954- self.our_hostname.clone().expect("hostname not configured"),
4955- self.greeting
4956- )
4957- )
4958- }
4959-
4960- // /// Ensure that the session has been initialized otherwise return an error
4961- fn check_initialized(&self, inner: &Inner) -> Result<(), Response<String>> {
4962- if inner.initialized.is_none() {
4963- return Err(smtp_response!(
4964- 500,
4965- 5,
4966- 5,
4967- 1,
4968- "It's polite to say EHLO first"
4969- ));
4970- }
4971- Ok(())
4972- }
4973-
4974- /// Process the SMTP command returning the action sometimes with a callback
4975- /// that the implementor needs to take.
4976- pub fn next(&mut self, req: Option<&Request<String>>) -> Action {
4977- let mut inner = self.inner.lock().unwrap();
4978- if let Some(req) = req {
4979- inner.history.push(req.clone());
4980- }
4981- match req {
4982- None => {
4983- tracing::info!("Sending initial greeting");
4984- Action::Send(smtp_response!(
4985- 220,
4986- 2,
4987- 0,
4988- 0,
4989- format!(
4990- "{} {}",
4991- self.our_hostname.clone().unwrap_or_default(),
4992- self.greeting
4993- )
4994- ))
4995- }
4996- Some(Request::Ehlo { host }) => {
4997- match Host::parse(&parse_host(host)) {
4998- Ok(hostname) => {
4999- inner.hostname = Some(hostname);
5000- }
5001- Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
5002- };
5003- inner.reset();
5004- inner.initialized = Some(Mode::Extended);
5005- let mut resp = EhloResponse::new(format!("Hello {}", host));
5006- resp.capabilities = self.capabilities;
5007- resp.size = self.maximum_size as usize;
5008- if self.flags.authentication {
5009- resp.auth_mechanisms = smtp_proto::AUTH_PLAIN;
5010- }
5011- Action::Send(Response::Ehlo(resp))
5012- }
5013- Some(Request::Lhlo { host }) => {
5014- match Host::parse(&parse_host(host)) {
5015- Ok(hostname) => {
5016- inner.hostname = Some(hostname);
5017- }
5018- Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
5019- };
5020- inner.reset();
5021- inner.initialized = Some(Mode::Legacy);
5022- Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
5023- }
5024- Some(Request::Helo { host }) => {
5025- match Host::parse(&parse_host(host)) {
5026- Ok(hostname) => {
5027- inner.hostname = Some(hostname);
5028- }
5029- Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
5030- };
5031- inner.reset();
5032- inner.initialized = Some(Mode::Legacy);
5033- Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
5034- }
5035- Some(Request::Mail { from }) => {
5036- if let Some(err) = self.check_initialized(&inner).err() {
5037- return Action::Send(err);
5038- }
5039- let mail_from = match EmailAddress::from_str(&from.address) {
5040- Ok(addr) => addr,
5041- Err(e) => {
5042- return Action::Send(smtp_response!(
5043- 500,
5044- 0,
5045- 0,
5046- 0,
5047- format!("cannot parse: {} {}", from.address, e)
5048- ))
5049- }
5050- };
5051- inner.mail_from = Some(mail_from.clone());
5052- Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
5053- }
5054- Some(Request::Rcpt { to }) => {
5055- if let Some(err) = self.check_initialized(&inner).err() {
5056- return Action::Send(err);
5057- }
5058- let rcpt_to = match EmailAddress::from_str(to.address.as_str()) {
5059- Ok(rcpt_to) => rcpt_to,
5060- Err(e) => {
5061- return Action::Send(smtp_response!(
5062- 500,
5063- 0,
5064- 0,
5065- 0,
5066- format!("cannot parse: {} {}", to.address, e)
5067- ))
5068- }
5069- };
5070- if let Some(ref mut rcpts) = inner.rcpt_to {
5071- rcpts.push(rcpt_to.clone());
5072- } else {
5073- inner.rcpt_to = Some(vec![rcpt_to.clone()]);
5074- }
5075- Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
5076- }
5077- Some(Request::Bdat {
5078- chunk_size: _,
5079- is_last: _,
5080- }) => {
5081- if let Some(err) = self.check_initialized(&inner).err() {
5082- return Action::Send(err);
5083- }
5084- tracing::info!("Starting binary data transfer");
5085- Action::Message {
5086- initial_response: smtp_response!(
5087- 354,
5088- 0,
5089- 0,
5090- 0,
5091- "Starting BDAT data transfer".to_string()
5092- ),
5093- cb: SetMessage {
5094- inner: self.inner.clone(),
5095- capabilities: Capabilities {
5096- mode: inner.initialized.clone(),
5097- capabilities: self.capabilities,
5098- },
5099- },
5100- }
5101- }
5102- // After an AUTH command has been successfully completed, no more
5103- // AUTH commands may be issued in the same session. After a
5104- // successful AUTH command completes, a server MUST reject any
5105- // further AUTH commands with a 503 reply.
5106- Some(Request::Auth {
5107- mechanism,
5108- initial_response,
5109- }) => {
5110- if let Some(err) = self.check_initialized(&inner).err() {
5111- return Action::Send(err);
5112- }
5113- if self.flags.authentication {
5114- if *mechanism != smtp_proto::AUTH_PLAIN {
5115- // only plain auth is supported
5116- return Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported"));
5117- }
5118- let auth_data = match AuthData::try_from(initial_response.as_str()) {
5119- Ok(auth_data) => auth_data,
5120- Err(e) => return Action::Send(e.into()),
5121- };
5122- Action::PlainAuth {
5123- authcid: auth_data.authcid(),
5124- authzid: auth_data.authzid(),
5125- password: auth_data.passwd(),
5126- cb: PlainAuth(self.inner.clone()),
5127- }
5128- } else {
5129- Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported"))
5130- }
5131- }
5132- Some(Request::Noop { value: _ }) => {
5133- if let Some(err) = self.check_initialized(&inner).err() {
5134- return Action::Send(err);
5135- }
5136- Action::Send(smtp_response!(250, 0, 0, 0, "OK".to_string()))
5137- }
5138- Some(Request::Vrfy { value }) => {
5139- if let Some(err) = self.check_initialized(&inner).err() {
5140- return Action::Send(err);
5141- }
5142- if self.flags.vrfy {
5143- let address = match EmailAddress::from_str(value) {
5144- Ok(addr) => addr,
5145- Err(e) => {
5146- return Action::Send(smtp_response!(
5147- 500,
5148- 0,
5149- 0,
5150- 0,
5151- format!("cannot parse: {} {}", value, e)
5152- ))
5153- }
5154- };
5155- Action::Verify {
5156- address,
5157- cb: Verify,
5158- }
5159- } else {
5160- Action::Send(smtp_response!(500, 0, 0, 0, "VRFY Unavailable"))
5161- }
5162- }
5163- Some(Request::Expn { value }) => {
5164- if let Some(err) = self.check_initialized(&inner).err() {
5165- return Action::Send(err);
5166- }
5167- if self.flags.expn && inner.authenticated_id.is_some() {
5168- Action::Expand {
5169- address: value.clone(),
5170- cb: Expand,
5171- }
5172- } else {
5173- Action::Send(smtp_response!(500, 0, 0, 0, "EXPN Unavailable"))
5174- }
5175- }
5176- Some(Request::Help { value }) => {
5177- if let Some(err) = self.check_initialized(&inner).err() {
5178- return Action::Send(err);
5179- }
5180- if value.is_empty() {
5181- Action::Send(smtp_response!(250, 0, 0, 0, self.help_banner))
5182- } else {
5183- Action::Send(smtp_response!(
5184- 500,
5185- 0,
5186- 0,
5187- 0,
5188- format!("Help for {} is not currently available", value)
5189- ))
5190- }
5191- }
5192- Some(Request::Etrn { name: _ }) => {
5193- Action::Send(smtp_response!(500, 0, 0, 0, "ETRN is not supported"))
5194- }
5195- Some(Request::Atrn { domains: _ }) => {
5196- Action::Send(smtp_response!(500, 0, 0, 0, "ATRN is not supported"))
5197- }
5198- Some(Request::Burl { uri: _, is_last: _ }) => {
5199- Action::Send(smtp_response!(500, 0, 0, 0, "BURL is not supported"))
5200- }
5201- Some(Request::StartTls) => {
5202- if self.flags.starttls && !self.tls_active {
5203- Action::StartTls(smtp_response!(220, 0, 0, 0, "Go ahead"))
5204- } else if self.flags.starttls && self.tls_active {
5205- Action::Send(tls_already_active())
5206- } else {
5207- Action::Send(smtp_response!(
5208- 500,
5209- 0,
5210- 0,
5211- 0,
5212- format!("STARTTLS is not supported")
5213- ))
5214- }
5215- }
5216- Some(Request::Data) => {
5217- if let Some(err) = self.check_initialized(&inner).err() {
5218- return Action::Send(err);
5219- }
5220- tracing::info!("Starting data transfer");
5221- Action::Message {
5222- initial_response: smtp_response!(
5223- 354,
5224- 0,
5225- 0,
5226- 0,
5227- "Reading data input, end the message with <CRLF>.<CRLF>".to_string()
5228- ),
5229- cb: SetMessage {
5230- inner: self.inner.clone(),
5231- capabilities: Capabilities {
5232- mode: inner.initialized.clone(),
5233- capabilities: self.capabilities,
5234- },
5235- },
5236- }
5237- }
5238- Some(Request::Rset) => {
5239- if let Some(err) = self.check_initialized(&inner).err() {
5240- return Action::Send(err);
5241- }
5242- inner.reset();
5243- Action::Send(smtp_response!(200, 0, 0, 0, "".to_string()))
5244- }
5245- Some(Request::Quit) => Action::Quit(smtp_response!(221, 0, 0, 0, "Ciao!".to_string())),
5246- }
5247- }
5248- }
5249-
5250- #[cfg(test)]
5251- mod test {
5252-
5253- use base64::engine::general_purpose::STANDARD;
5254- use base64::prelude::*;
5255- use smtp_proto::MailFrom;
5256-
5257- use super::*;
5258-
5259- const EXAMPLE_HOSTNAME: &str = "example.org";
5260-
5261- fn equal(actual: &Action, expected: &Action) -> bool {
5262- let is_equal = match actual {
5263- Action::Send(response) => {
5264- matches!(expected, Action::Send(other) if response.eq(other))
5265- }
5266- Action::SendMany(actual) => match expected {
5267- Action::SendMany(expected) => actual.iter().enumerate().all(|(i, resp)| {
5268- if let Some(expected_resp) = expected.get(i) {
5269- resp.eq(expected_resp)
5270- } else {
5271- false
5272- }
5273- }),
5274- _ => false,
5275- },
5276- Action::Message {
5277- initial_response: _,
5278- cb: _,
5279- } => todo!(),
5280- Action::Envelope {
5281- initial_response: _,
5282- envelope: _,
5283- } => {
5284- matches!(
5285- expected,
5286- Action::Envelope {
5287- initial_response: _,
5288- envelope: _
5289- }
5290- )
5291- }
5292- Action::PlainAuth {
5293- authcid: _,
5294- authzid: _,
5295- password: _,
5296- cb: _,
5297- } => todo!(),
5298- Action::Verify { address: _, cb: _ } => todo!(),
5299- Action::Expand { address: _, cb: _ } => todo!(),
5300- Action::StartTls(_response) => todo!(),
5301- Action::Quit(response) => {
5302- matches!(expected, Action::Quit(other) if response.eq(other))
5303- }
5304- };
5305-
5306- if !is_equal {
5307- println!("Responses Differ:");
5308- println!("Expected:");
5309- println!("{}", expected);
5310- println!("Actual:");
5311- println!("{}", actual);
5312- return false;
5313- };
5314-
5315- true
5316- }
5317-
5318- #[test]
5319- fn session_greeting() {
5320- let mut session = Session::default();
5321- assert!(matches!(session.next(None), Action::Send(_)))
5322- }
5323-
5324- #[test]
5325- fn session_hello_quit() {
5326- let mut session = Session::default();
5327- assert!(equal(
5328- &session.next(Some(&Request::Helo {
5329- host: EXAMPLE_HOSTNAME.to_string(),
5330- })),
5331- &Action::Send(smtp_response!(
5332- 250,
5333- 0,
5334- 0,
5335- 0,
5336- String::from("Hello example.org")
5337- )),
5338- ));
5339- assert!(equal(
5340- &session.next(Some(&Request::Quit {})),
5341- &Action::Quit(smtp_response!(221, 0, 0, 0, String::from("Ciao!"))),
5342- ));
5343-
5344- assert!(session
5345- .inner
5346- .lock()
5347- .unwrap()
5348- .hostname
5349- .as_ref()
5350- .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
5351- }
5352-
5353- #[test]
5354- fn session_command_with_no_helo() {
5355- let mut session = Session::default();
5356- assert!(equal(
5357- &session.next(Some(&Request::Mail {
5358- from: MailFrom {
5359- address: String::from("fuu@example.org"),
5360- ..Default::default()
5361- }
5362- })),
5363- &Action::Send(smtp_response!(
5364- 500,
5365- 5,
5366- 5,
5367- 1,
5368- String::from("It's polite to say EHLO first")
5369- ))
5370- ))
5371- }
5372-
5373- #[test]
5374- fn session_authenticate() {
5375- let session = &mut Session::default().authentication(true);
5376- assert!(equal(
5377- &session.next(Some(&Request::Helo {
5378- host: EXAMPLE_HOSTNAME.to_string(),
5379- })),
5380- &Action::Send(smtp_response!(
5381- 250,
5382- 0,
5383- 0,
5384- 0,
5385- String::from("Hello example.org")
5386- )),
5387- ));
5388-
5389- {
5390- let auth = session.next(Some(&Request::Auth {
5391- mechanism: smtp_proto::AUTH_PLAIN,
5392- initial_response: STANDARD.encode(b"\0hello\0world"),
5393- }));
5394- match auth {
5395- Action::PlainAuth {
5396- authcid,
5397- authzid,
5398- password,
5399- cb,
5400- } => {
5401- assert!(authcid == "hello");
5402- assert!(authzid == "hello");
5403- assert!(password == "world");
5404- assert!(equal(
5405- &cb.call(Ok(authcid.clone())),
5406- &Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
5407- ));
5408- }
5409- _ => panic!("Unexpected response"),
5410- };
5411- };
5412-
5413- assert!(session
5414- .inner
5415- .lock()
5416- .unwrap()
5417- .authenticated_id
5418- .as_ref()
5419- .is_some_and(|id| id == "hello"));
5420- }
5421-
5422- #[test]
5423- fn session_expand() {
5424- let session = &mut Session::default().authentication(true).expn_enabled(true);
5425- session.inner = Arc::new(Mutex::new(Inner {
5426- initialized: Some(Mode::Extended),
5427- authenticated_id: Some("hello".to_string()),
5428- ..Default::default()
5429- }));
5430- match session.next(Some(&Request::Expn {
5431- value: String::from("group@baz.com"),
5432- })) {
5433- Action::Expand { address: _, cb } => {
5434- assert!(equal(
5435- &cb.call(Ok(vec![
5436- EmailAddress::new_unchecked("fuu@bar.com"),
5437- EmailAddress::new_unchecked("baz@qux.com")
5438- ])),
5439- &Action::SendMany(vec![
5440- smtp_response!(250, 0, 0, 0, "OK"),
5441- smtp_response!(250, 0, 0, 0, "fuu@bar.com"),
5442- smtp_response!(250, 0, 0, 0, "baz@qux.com")
5443- ])
5444- ));
5445- }
5446- _ => panic!("Unexpected response"),
5447- };
5448- }
5449-
5450- #[test]
5451- fn session_verify() {
5452- let session = &mut Session::default().authentication(true).vrfy_enabled(true);
5453- session.inner = Arc::new(Mutex::new(Inner {
5454- initialized: Some(Mode::Extended),
5455- authenticated_id: Some("hello".to_string()),
5456- ..Default::default()
5457- }));
5458- match session.next(Some(&Request::Vrfy {
5459- value: String::from("qux@baz.com"),
5460- })) {
5461- Action::Verify { address, cb } => {
5462- assert!(address.to_string() == "qux@baz.com");
5463- assert!(equal(
5464- &cb.call(Ok(())),
5465- &Action::Send(smtp_response!(200, 0, 0, 0, "OK"))
5466- ));
5467- }
5468- _ => panic!("Unexpected response"),
5469- };
5470- }
5471-
5472- #[test]
5473- fn session_non_ascii_characters_legacy_smtp() {
5474- let session = &mut Session::default();
5475- // non-extended sessions cannot accept non-ascii characters
5476- session.inner = Arc::new(Mutex::new(Inner {
5477- initialized: Some(Mode::Legacy),
5478- hostname: Some(Host::parse("example.org").unwrap()),
5479- authenticated_id: Some("hello".to_string()),
5480- mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
5481- rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
5482- ..Default::default()
5483- }));
5484- match session.next(Some(&Request::Data {})) {
5485- Action::Message {
5486- initial_response,
5487- cb,
5488- } => {
5489- assert!(equal(
5490- &Action::Send(initial_response),
5491- &Action::Send(smtp_response!(
5492- 354,
5493- 0,
5494- 0,
5495- 0,
5496- "Reading data input, end the message with <CRLF>.<CRLF>"
5497- ))
5498- ));
5499- let action = cb.call(
5500- r#"
5501- Subject: Hello World
5502- 😍😍😍
5503- "#
5504- .as_bytes(),
5505- );
5506- assert!(equal(
5507- &action,
5508- &Action::Send(smtp_response!(
5509- 500,
5510- 0,
5511- 0,
5512- 0,
5513- "Non ASCII characters found in message body"
5514- ))
5515- ))
5516- }
5517- _ => panic!("Unexpected response"),
5518- };
5519- }
5520-
5521- #[test]
5522- fn session_non_ascii_characters_extended_smtp() {
5523- let session = &mut Session::default();
5524- // non-extended sessions cannot accept non-ascii characters
5525- session.inner = Arc::new(Mutex::new(Inner {
5526- initialized: Some(Mode::Extended),
5527- hostname: Some(Host::parse("example.org").unwrap()),
5528- authenticated_id: Some("hello".to_string()),
5529- mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
5530- rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
5531- ..Default::default()
5532- }));
5533- match session.next(Some(&Request::Data {})) {
5534- Action::Message {
5535- initial_response,
5536- cb,
5537- } => {
5538- assert!(equal(
5539- &Action::Send(initial_response),
5540- &Action::Send(smtp_response!(
5541- 354,
5542- 0,
5543- 0,
5544- 0,
5545- "Reading data input, end the message with <CRLF>.<CRLF>"
5546- ))
5547- ));
5548- let action = cb.call(
5549- r#"
5550- Subject: Hello World
5551- 😍😍😍
5552- "#
5553- .as_bytes(),
5554- );
5555- assert!(equal(
5556- &action,
5557- &Action::Envelope {
5558- initial_response: smtp_response!(250, 0, 0, 0, "OK"),
5559- envelope: Envelope {
5560- body: Message::default(),
5561- mail_from: EmailAddress::new_unchecked("fuu@bar.com"),
5562- rcpt_to: vec![],
5563- hostname: Host::Domain(String::from("bar.com"))
5564- }
5565- }
5566- ))
5567- }
5568- _ => panic!("Unexpected response"),
5569- };
5570- }
5571-
5572- #[test]
5573- fn session_message_body_ok() {
5574- let session = &mut Session::default();
5575- // non-extended sessions cannot accept non-ascii characters
5576- session.inner = Arc::new(Mutex::new(Inner {
5577- hostname: Some(Host::parse("example.org").unwrap()),
5578- initialized: Some(Mode::Extended),
5579- authenticated_id: Some("hello".to_string()),
5580- mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
5581- rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
5582- ..Default::default()
5583- }));
5584- {
5585- match session.next(Some(&Request::Data {})) {
5586- Action::Message {
5587- initial_response,
5588- cb,
5589- } => {
5590- assert!(equal(
5591- &Action::Send(initial_response),
5592- &Action::Send(smtp_response!(
5593- 354,
5594- 0,
5595- 0,
5596- 0,
5597- "Reading data input, end the message with <CRLF>.<CRLF>"
5598- ))
5599- ));
5600- let action = cb.call(
5601- r#"To: <baz@qux.com>
5602- Subject: Hello World
5603-
5604- This is an e-mail from a test case!
5605-
5606- Note that it doesn't end with a "." since that parsing happens as part of the
5607- transport rather than the session. 🩷
5608- "#
5609- .as_bytes(),
5610- );
5611- assert!(equal(
5612- &action,
5613- &Action::Envelope {
5614- initial_response: smtp_response!(250, 0, 0, 0, "OK"),
5615- envelope: Envelope {
5616- body: Message::default(),
5617- mail_from: EmailAddress::new_unchecked("fuu@bar.com"),
5618- rcpt_to: vec![],
5619- hostname: Host::Domain("example.org".to_string())
5620- }
5621- }
5622- ));
5623- }
5624- _ => panic!("Unexpected response"),
5625- };
5626- };
5627- }
5628- }
5629 diff --git a/maitred/src/transport.rs b/maitred/src/transport.rs
5630deleted file mode 100644
5631index ae847f6..0000000
5632--- a/maitred/src/transport.rs
5633+++ /dev/null
5634 @@ -1,270 +0,0 @@
5635- use std::{fmt::Display, io::Write};
5636-
5637- use bytes::{Bytes, BytesMut};
5638- use smtp_proto::request::receiver::{BdatReceiver, DataReceiver, RequestReceiver};
5639- use smtp_proto::Error as SmtpError;
5640- use smtp_proto::Request;
5641- use tokio_util::codec::{Decoder, Encoder};
5642-
5643- use crate::session::Response;
5644-
5645- /// Error that occurred at the transport layer
5646- #[derive(Debug, thiserror::Error)]
5647- pub enum TransportError {
5648- /// Returned when a client attempts to send multiple commands sequentially
5649- /// to the server without waiting for a response but piplining isn't
5650- /// enabled.
5651- #[error("Pipelining is not enabled")]
5652- PipelineNotEnabled,
5653- /// An error generated from the underlying SMTP protocol
5654- #[error("Smtp failure: {0}")]
5655- Smtp(#[from] SmtpError),
5656- /// An IO related error such as not being able to bind to a TCP socket
5657- #[error("Io: {0}")]
5658- Io(#[from] std::io::Error),
5659- }
5660-
5661- struct Wrapper<'a>(&'a mut BytesMut);
5662-
5663- impl Write for Wrapper<'_> {
5664- fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
5665- self.0.extend_from_slice(buf);
5666- Ok(buf.len())
5667- }
5668-
5669- fn flush(&mut self) -> std::io::Result<()> {
5670- Ok(())
5671- }
5672- }
5673-
5674- pub(crate) enum Receiver {
5675- Data(DataReceiver),
5676- Bdat(BdatReceiver),
5677- }
5678-
5679- /// Command from the client with an optional attached payload.
5680- #[derive(Debug)]
5681- pub enum Command {
5682- /// One or more requests depending if PIPELINING is enabled.
5683- Requests(Vec<Request<String>>),
5684- /// Message payload possibily sent over multiple frames.
5685- Payload(Bytes),
5686- }
5687-
5688- impl Display for Command {
5689- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
5690- match self {
5691- Command::Requests(requests) => write!(f, "{:?}", requests),
5692- Command::Payload(payload) => write!(f, "Bytes ({})", payload.len()),
5693- }
5694- }
5695- }
5696-
5697- /// Low-level line oriented transport for handling SMTP connections.
5698- // TODO: BINARYMIME
5699- #[derive(Default)]
5700- pub struct Transport {
5701- receiver: Option<Box<Receiver>>,
5702- buf: Vec<u8>,
5703- pipelining: bool,
5704- }
5705-
5706- impl Clone for Transport {
5707- fn clone(&self) -> Self {
5708- Transport {
5709- receiver: None,
5710- buf: Vec::new(),
5711- pipelining: self.pipelining,
5712- }
5713- }
5714- }
5715-
5716- impl Transport {
5717- /// If the transport should allow piplining commands
5718- pub fn pipelining(mut self, enabled: bool) -> Self {
5719- self.pipelining = enabled;
5720- self
5721- }
5722- }
5723-
5724- impl Encoder<Response<String>> for Transport {
5725- type Error = TransportError;
5726-
5727- fn encode(&mut self, item: Response<String>, dst: &mut BytesMut) -> Result<(), Self::Error> {
5728- tracing::debug!("Writing response: {:?}", item);
5729- match item {
5730- Response::General(item) => {
5731- item.write(Wrapper(dst))?;
5732- }
5733- Response::Ehlo(item) => {
5734- item.write(Wrapper(dst))?;
5735- }
5736- }
5737- Ok(())
5738- }
5739- }
5740-
5741- impl Decoder for Transport {
5742- type Item = Command;
5743- type Error = TransportError;
5744-
5745- fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
5746- tracing::trace!("Decoding:\n{}", String::from_utf8_lossy(src));
5747-
5748- if src.is_empty() {
5749- tracing::debug!("Empty command received");
5750- return Ok(None);
5751- }
5752-
5753- if let Some(rec) = self.receiver.as_mut() {
5754- let chunk_size = src.len();
5755- tracing::debug!("Reading {} bytes of data stream", chunk_size);
5756- match rec.as_mut() {
5757- Receiver::Data(data_receiver) => {
5758- let chunk = src.split_to(src.len());
5759- if data_receiver.ingest(&mut chunk.iter(), &mut self.buf) {
5760- tracing::debug!("Finished parsing data stream");
5761- let payload = Bytes::copy_from_slice(&self.buf);
5762- self.buf.clear();
5763- self.receiver = None;
5764- return Ok(Some(Command::Payload(payload)));
5765- } else {
5766- return Ok(None);
5767- }
5768- }
5769- Receiver::Bdat(bdat_receiver) => {
5770- let chunk = src.split_to(src.len());
5771- if bdat_receiver.ingest(&mut chunk.iter(), &mut self.buf) {
5772- tracing::debug!("Finished parsing data stream");
5773- let payload = Bytes::copy_from_slice(&self.buf);
5774- self.buf.clear();
5775- self.receiver = None;
5776- return Ok(Some(Command::Payload(payload)));
5777- } else {
5778- return Ok(None);
5779- }
5780- }
5781- }
5782- };
5783-
5784- let mut r = RequestReceiver::default();
5785- let mut requests: Vec<Request<String>> = Vec::new();
5786- let mut iter = src.iter();
5787- 'outer: loop {
5788- match r.ingest(&mut iter, src) {
5789- Ok(request) => {
5790- if !requests.is_empty() && !self.pipelining {
5791- return Err(TransportError::PipelineNotEnabled);
5792- }
5793- requests.push(request);
5794- }
5795- Err(err) => {
5796- if matches!(err, smtp_proto::Error::NeedsMoreData { bytes_left: _ }) {
5797- break 'outer;
5798- } else {
5799- return Err(TransportError::Smtp(err));
5800- }
5801- }
5802- }
5803- }
5804-
5805- src.clear();
5806-
5807- let last = requests.last().expect("No data parsed");
5808- match last {
5809- Request::Bdat {
5810- chunk_size,
5811- is_last,
5812- } => {
5813- tracing::info!("Starting binary data transfer");
5814- self.receiver = Some(Box::new(Receiver::Bdat(BdatReceiver::new(
5815- *chunk_size,
5816- *is_last,
5817- ))));
5818- self.buf.clear();
5819- Ok(Some(Command::Requests(requests)))
5820- }
5821- Request::Data => {
5822- tracing::info!("Starting data transfer");
5823- self.receiver = Some(Box::new(Receiver::Data(DataReceiver::new())));
5824- self.buf.clear();
5825- Ok(Some(Command::Requests(requests)))
5826- }
5827- _ => Ok(Some(Command::Requests(requests))),
5828- }
5829- }
5830- }
5831-
5832- #[cfg(test)]
5833- mod test {
5834-
5835- use super::*;
5836-
5837- use bytes::BytesMut;
5838-
5839- #[test]
5840- fn sequential_commands() {
5841- let mut transport = Transport::default();
5842- match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
5843- Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
5844- _ => panic!(),
5845- };
5846- match transport.decode(&mut BytesMut::from("DATA\r\n")) {
5847- Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
5848- _ => panic!(),
5849- };
5850- match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
5851- Ok(None) => {}
5852- _ => panic!(),
5853- };
5854- match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
5855- Ok(None) => {}
5856- _ => panic!(),
5857- };
5858- match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
5859- Ok(None) => {}
5860- _ => panic!(),
5861- };
5862- match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
5863- Ok(Some(Command::Payload(_))) => {}
5864- _ => panic!(),
5865- };
5866- match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
5867- Ok(Some(Command::Requests(_))) => {}
5868- _ => panic!(),
5869- };
5870- }
5871-
5872- #[test]
5873- fn sequential_commands_pipeline() {
5874- let mut transport = Transport::default().pipelining(true);
5875- match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
5876- Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
5877- _ => panic!(),
5878- };
5879- match transport.decode(&mut BytesMut::from("DATA\r\n")) {
5880- Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
5881- _ => panic!(),
5882- };
5883- match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
5884- Ok(None) => {}
5885- _ => panic!(),
5886- };
5887- match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
5888- Ok(None) => {}
5889- _ => panic!(),
5890- };
5891- match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
5892- Ok(None) => {}
5893- _ => panic!(),
5894- };
5895- match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
5896- Ok(Some(Command::Payload(_))) => {}
5897- _ => panic!(),
5898- };
5899- match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
5900- Ok(Some(Command::Requests(_))) => {}
5901- _ => panic!(),
5902- };
5903- }
5904- }
5905 diff --git a/maitred/src/validation.rs b/maitred/src/validation.rs
5906deleted file mode 100644
5907index b81de23..0000000
5908--- a/maitred/src/validation.rs
5909+++ /dev/null
5910 @@ -1,86 +0,0 @@
5911- use std::net::IpAddr;
5912-
5913- use mail_auth::{AuthenticatedMessage, DkimResult, Resolver};
5914- use tokio::sync::MutexGuard;
5915-
5916- /// Wraps around mail_auth to do various email authentication / validation
5917- pub(crate) struct Validation<'a>(pub MutexGuard<'a, Resolver>);
5918-
5919- impl Validation<'_> {
5920- pub async fn verify_dkim(&self, message: &[u8]) -> bool {
5921- let authenticated = AuthenticatedMessage::parse(message).unwrap();
5922- self.0.verify_dkim(&authenticated).await.iter().all(|s| {
5923- match s.result() {
5924- DkimResult::Pass => {
5925- tracing::info!("DKIM Passed");
5926- true
5927- }
5928- DkimResult::Neutral(error) => {
5929- tracing::info!("DKIM Neutral: {}", error);
5930- false
5931- }
5932- DkimResult::Fail(error) => {
5933- tracing::info!("DKIM Failed: {}", error);
5934- false
5935- }
5936- DkimResult::PermError(error) => {
5937- tracing::info!("DKIM Permanent Error: {}", error);
5938- false
5939- }
5940- DkimResult::TempError(error) => {
5941- // TODO: queued retry?
5942- tracing::info!("DKIM Temporary Error: {}", error);
5943- false
5944- }
5945- DkimResult::None => {
5946- tracing::warn!("No DKIM Result");
5947- false
5948- }
5949- }
5950- })
5951- }
5952-
5953- pub async fn verify_spf(
5954- &self,
5955- ip: IpAddr,
5956- helo_domain: &str,
5957- host_domain: &str,
5958- mail_from: &str,
5959- ) -> bool {
5960- let output = self
5961- .0
5962- .verify_spf(ip, helo_domain, host_domain, mail_from)
5963- .await;
5964- match output.result() {
5965- mail_auth::SpfResult::Pass => {
5966- tracing::info!("SPF Passed");
5967- true
5968- }
5969- mail_auth::SpfResult::Fail => {
5970- tracing::info!("SPF Failed");
5971- false
5972- }
5973- mail_auth::SpfResult::SoftFail => {
5974- tracing::info!("SPF Soft Failure");
5975- false
5976- }
5977- mail_auth::SpfResult::Neutral => {
5978- tracing::info!("SPF Neutral");
5979- false
5980- }
5981- mail_auth::SpfResult::TempError => {
5982- // TODO Queued retry?
5983- tracing::info!("SPF Temporary Error");
5984- false
5985- }
5986- mail_auth::SpfResult::PermError => {
5987- tracing::info!("SPF Permanent Error");
5988- false
5989- }
5990- mail_auth::SpfResult::None => {
5991- tracing::info!("No SPF Result");
5992- false
5993- }
5994- }
5995- }
5996- }
5997 diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs
5998deleted file mode 100644
5999index 7ade63b..0000000
6000--- a/maitred/src/verify.rs
6001+++ /dev/null
6002 @@ -1,78 +0,0 @@
6003- use std::future::Future;
6004-
6005- use async_trait::async_trait;
6006- use email_address::EmailAddress;
6007- use smtp_proto::Response as SmtpResponse;
6008-
6009- use crate::session::Response;
6010- use crate::smtp_response;
6011-
6012- /// An error encountered while verifying an e-mail address
6013- #[derive(Debug, thiserror::Error)]
6014- pub enum VerifyError {
6015- /// Indicates an unspecified error that occurred during expansion
6016- #[error("Internal Server Error: {0}")]
6017- Server(String),
6018- /// Indicates that no group exists with the specified name
6019- #[error("Group Not Found: {0}")]
6020- NotFound(String),
6021- /// Indicates that the input as ambigious and multiple addresses are
6022- /// associated with the string.
6023- #[error("Name is Ambiguous: {email}")]
6024- Ambiguous {
6025- email: EmailAddress,
6026- alternatives: Vec<EmailAddress>,
6027- },
6028- }
6029-
6030- #[allow(clippy::from_over_into)]
6031- impl Into<Response<String>> for VerifyError {
6032- fn into(self) -> Response<String> {
6033- match self {
6034- VerifyError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()),
6035- VerifyError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()),
6036- VerifyError::Ambiguous {
6037- email: _,
6038- alternatives: _,
6039- } => smtp_response!(500, 0, 0, 0, self.to_string()),
6040- }
6041- }
6042- }
6043-
6044- /// Verify that the given e-mail address exists on the server. Servers may
6045- /// choose to implement nothing or not use this option at all if desired.
6046- #[async_trait]
6047- pub trait Verify: Sync + Send {
6048- /// Verify the e-mail address on the server
6049- async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError>;
6050- }
6051-
6052- /// VerifyFunc wraps an async closure implementing the Verify trait.
6053- /// # Example
6054- /// ```rust
6055- /// use maitred::verify::VerifyFunc;
6056- /// use maitred::email_address::EmailAddress;
6057- ///
6058- /// let verify = VerifyFunc(|address: &EmailAddress| {
6059- /// async move {
6060- /// Ok(())
6061- /// }
6062- /// });
6063- ///
6064- /// ```
6065- pub struct VerifyFunc<F, T>(pub F)
6066- where
6067- F: Fn(&EmailAddress) -> T + Sync + Send,
6068- T: Future<Output = Result<(), VerifyError>> + Send;
6069-
6070- #[async_trait]
6071- impl<F, T> Verify for VerifyFunc<F, T>
6072- where
6073- F: Fn(&EmailAddress) -> T + Sync + Send,
6074- T: Future<Output = Result<(), VerifyError>> + Send,
6075- {
6076- async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError> {
6077- let f = (self.0)(address);
6078- f.await
6079- }
6080- }
6081 diff --git a/maitred/src/worker.rs b/maitred/src/worker.rs
6082deleted file mode 100644
6083index 6f224f8..0000000
6084--- a/maitred/src/worker.rs
6085+++ /dev/null
6086 @@ -1,116 +0,0 @@
6087- use std::sync::Arc;
6088- use std::{iter, time::Duration};
6089-
6090- use crossbeam_deque::{Injector, Stealer, Worker as WorkQueue};
6091- use mail_auth::Resolver;
6092- use tokio::sync::{mpsc::Receiver, Mutex};
6093-
6094- use crate::delivery::Delivery;
6095- use crate::milter::Milter;
6096- use crate::rewrite::Rewrite;
6097- use crate::server::ServerError;
6098- use crate::session::Envelope;
6099- use crate::validation::Validation;
6100-
6101- const HEADER_DKIM_RESULT: &str = "Maitred-Dkim-Result";
6102-
6103- /// Worker is responsible for all asynchronous message processing after a
6104- /// session has been completed. It will handle the following operations:
6105- ///
6106- /// Sequentially applying milters in the order they were configured
6107- /// Running DKIM verification
6108- /// ARC Verficiation
6109- pub(crate) struct Worker {
6110- pub milter: Option<Arc<dyn Milter>>,
6111- pub delivery: Option<Arc<dyn Delivery>>,
6112- pub global_queue: Arc<Injector<Envelope>>,
6113- pub stealers: Vec<Stealer<Envelope>>,
6114- pub local_queue: Arc<Mutex<WorkQueue<Envelope>>>,
6115- pub shutdown_rx: Receiver<bool>,
6116- pub resolver: Option<Arc<Mutex<Resolver>>>,
6117- pub dkim_verification: bool,
6118- }
6119-
6120- impl Worker {
6121- async fn next_packet(&self) -> Option<Envelope> {
6122- let local_queue = self.local_queue.lock().await;
6123- local_queue.pop().or_else(|| {
6124- iter::repeat_with(|| {
6125- self.global_queue
6126- .steal_batch_and_pop(&local_queue)
6127- .or_else(|| self.stealers.iter().map(|s| s.steal()).collect())
6128- })
6129- .find(|s| !s.is_retry())
6130- .and_then(|s| s.success())
6131- })
6132- }
6133-
6134- pub async fn process(&mut self) -> Result<(), ServerError> {
6135- let mut ticker =
6136- tokio::time::interval_at(tokio::time::Instant::now(), Duration::from_millis(800));
6137-
6138- loop {
6139- if let Ok(Some(_)) =
6140- tokio::time::timeout(Duration::from_millis(100), self.shutdown_rx.recv()).await
6141- {
6142- break Ok(());
6143- }
6144-
6145- if let Some(envelope) = self.next_packet().await {
6146- let mut message = envelope.body;
6147- let message_bytes = message.raw_message();
6148- let message_id = message
6149- .message_id()
6150- .map(|id| id.to_string())
6151- .unwrap_or(String::from("Unknown Message"));
6152- let mut dkim_passed: Option<bool> = None;
6153- if self.dkim_verification {
6154- tracing::info!("DKIM Verification for {}", message_id);
6155- let resolver = self.resolver.as_ref().expect("Resolver not configured");
6156- let resolver = resolver.lock().await;
6157- let passed = Validation(resolver).verify_dkim(message_bytes).await;
6158- dkim_passed = Some(passed);
6159- }
6160- if let Some(milter) = &self.milter {
6161- tracing::info!("Applying Milter to message {}", message_id);
6162- match milter.apply(&message).await {
6163- Ok(modified) => {
6164- tracing::info!("Milter finished successfully");
6165- message = modified;
6166- }
6167- Err(err) => {
6168- tracing::warn!("Milter failed to apply: {:?}", err);
6169- }
6170- };
6171- }
6172-
6173- let mut modified_message_bytes = message.raw_message().to_vec();
6174- let mut modified = Rewrite::new(None, &mut modified_message_bytes);
6175- if let Some(dkim_passed) = dkim_passed {
6176- modified.set_header(HEADER_DKIM_RESULT.into(), &dkim_passed.to_string())
6177- }
6178-
6179- let to_deliver = modified.message();
6180-
6181- if let Some(delivery) = &self.delivery {
6182- tracing::info!("Delivering message {}", message_id);
6183- match delivery
6184- .deliver(&Envelope {
6185- body: to_deliver.into_owned(),
6186- mail_from: envelope.mail_from.clone(),
6187- rcpt_to: envelope.rcpt_to.clone(),
6188- hostname: envelope.hostname.clone(),
6189- })
6190- .await
6191- {
6192- Ok(_) => tracing::info!("Message successfully delivered"),
6193- // TODO: Implement retry here
6194- Err(err) => tracing::warn!("Message could not be delievered: {:?}", err),
6195- }
6196- }
6197- } else {
6198- ticker.tick().await;
6199- }
6200- }
6201- }
6202- }
6203 diff --git a/src/authentication.rs b/src/authentication.rs
6204new file mode 100644
6205index 0000000..8b9766f
6206--- /dev/null
6207+++ b/src/authentication.rs
6208 @@ -0,0 +1,50 @@
6209+ use std::net::IpAddr;
6210+
6211+ use mail_auth::{AuthenticatedMessage, AuthenticationResults, Resolver};
6212+ use tokio::sync::Mutex;
6213+
6214+ /// Perform various mail authentication
6215+ pub struct Authentication(Mutex<Resolver>);
6216+
6217+ impl Default for Authentication {
6218+ fn default() -> Self {
6219+ Authentication(Mutex::new(Resolver::new_system_conf().unwrap()))
6220+ }
6221+ }
6222+
6223+ impl Authentication {
6224+ pub fn results(hostname: &str) -> AuthenticationResults {
6225+ AuthenticationResults::new(hostname)
6226+ }
6227+
6228+ pub async fn verify_spf<'a>(
6229+ &self,
6230+ results: AuthenticationResults<'a>,
6231+ ip_addr: IpAddr,
6232+ helo_domain: &str,
6233+ host_domain: &str,
6234+ mail_from: &str,
6235+ ) -> AuthenticationResults<'a> {
6236+ let output = self
6237+ .0
6238+ .lock()
6239+ .await
6240+ .verify_spf(ip_addr, helo_domain, host_domain, mail_from)
6241+ .await;
6242+ tracing::info!("SPF result: {}", output.result());
6243+ results.with_spf_ehlo_result(&output, ip_addr, helo_domain)
6244+ }
6245+
6246+ pub async fn verify_dkim<'a>(
6247+ &self,
6248+ results: AuthenticationResults<'a>,
6249+ message: &[u8],
6250+ ) -> AuthenticationResults<'a> {
6251+ let message = AuthenticatedMessage::parse(message).unwrap();
6252+ let dkim_results = self.0.lock().await.verify_dkim(&message).await;
6253+ dkim_results.iter().for_each(|dkim_result| {
6254+ tracing::info!("DKIM Result: {}", dkim_result.result());
6255+ });
6256+ results.with_dkim_results(&dkim_results, message.from())
6257+ }
6258+ }
6259 diff --git a/src/crypto.rs b/src/crypto.rs
6260new file mode 100644
6261index 0000000..0b8cdda
6262--- /dev/null
6263+++ b/src/crypto.rs
6264 @@ -0,0 +1,8 @@
6265+ use rustls::crypto::{aws_lc_rs, CryptoProvider};
6266+
6267+ /// Initialize the default crypto provider, required if using opportunistic TLS.
6268+ pub fn init() {
6269+ if CryptoProvider::get_default().is_none() {
6270+ CryptoProvider::install_default(aws_lc_rs::default_provider()).unwrap()
6271+ }
6272+ }
6273 diff --git a/src/delivery.rs b/src/delivery.rs
6274new file mode 100644
6275index 0000000..34a7ba3
6276--- /dev/null
6277+++ b/src/delivery.rs
6278 @@ -0,0 +1,60 @@
6279+ use std::{future::Future, io::Error as IoError};
6280+
6281+ use async_trait::async_trait;
6282+ use smtp_proto::Response as SmtpResponse;
6283+
6284+ use crate::{session::Envelope, smtp_response, Response};
6285+
6286+ /// Error that occurred delivering mail
6287+ #[derive(Debug, thiserror::Error)]
6288+ pub enum DeliveryError {
6289+ /// Indicates an unspecified error that occurred during milting.
6290+ #[error("Internal Server Error: {0}")]
6291+ Server(String),
6292+ #[error("IO Error: {0}")]
6293+ Io(#[from] IoError),
6294+ #[error("SPF Refjection")]
6295+ SpfRejection,
6296+ }
6297+
6298+ impl From<DeliveryError> for Response<String> {
6299+ fn from(val: DeliveryError) -> Self {
6300+ smtp_response!(500, 0, 0, 0, &val.to_string())
6301+ }
6302+ }
6303+
6304+ /// Delivery is the final stage of accepting an e-mail and may be invoked
6305+ /// multiple times depending on the server configuration.
6306+ #[async_trait]
6307+ pub trait Delivery: Sync + Send {
6308+ /// Persist and verify an e-mail message
6309+ async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError>;
6310+ }
6311+
6312+ /// DeliveryFunc wraps an async closure implementing the Delivery trait.
6313+ /// ```rust
6314+ /// use maitred::delivery::DeliveryFunc;
6315+ /// use maitred::Envelope;
6316+ ///
6317+ /// let delivery = DeliveryFunc(|message: &Envelope| {
6318+ /// async move {
6319+ /// Ok(())
6320+ /// }
6321+ /// });
6322+ /// ```
6323+ pub struct DeliveryFunc<F, T>(pub F)
6324+ where
6325+ F: Fn(&Envelope) -> T + Sync + Send,
6326+ T: Future<Output = Result<(), DeliveryError>> + Send;
6327+
6328+ #[async_trait]
6329+ impl<F, T> Delivery for DeliveryFunc<F, T>
6330+ where
6331+ F: Fn(&Envelope) -> T + Sync + Send,
6332+ T: Future<Output = Result<(), DeliveryError>> + Send,
6333+ {
6334+ async fn deliver(&self, message: &Envelope) -> Result<(), DeliveryError> {
6335+ let f = (self.0)(message);
6336+ f.await
6337+ }
6338+ }
6339 diff --git a/src/expand.rs b/src/expand.rs
6340new file mode 100644
6341index 0000000..b810ecb
6342--- /dev/null
6343+++ b/src/expand.rs
6344 @@ -0,0 +1,70 @@
6345+ use std::future::Future;
6346+
6347+ use async_trait::async_trait;
6348+ use email_address::EmailAddress;
6349+ use smtp_proto::Response as SmtpResponse;
6350+
6351+ use crate::session::Response;
6352+ use crate::smtp_response;
6353+
6354+ /// An error encountered while expanding a mail address
6355+ #[derive(Debug, thiserror::Error)]
6356+ pub enum ExpansionError {
6357+ /// Indicates an unspecified error that occurred during expansion.
6358+ #[error("Internal Server Error: {0}")]
6359+ Server(String),
6360+ /// Indicates that no group exists with the specified name
6361+ #[error("Group Not Found: {0}")]
6362+ NotFound(String),
6363+ }
6364+ #[allow(clippy::from_over_into)]
6365+ impl Into<Response<String>> for ExpansionError {
6366+ fn into(self) -> Response<String> {
6367+ match self {
6368+ ExpansionError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()),
6369+ ExpansionError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()),
6370+ }
6371+ }
6372+ }
6373+
6374+ /// Expands a string representing a mailing list to an array of the associated
6375+ /// addresses within the list if it exists. NOTE: That this function should
6376+ /// only be called with proper authentication otherwise it could be used to
6377+ /// harvest e-mail addresses.
6378+ #[async_trait]
6379+ pub trait Expansion: Sync + Send {
6380+ /// Expand the group into an array of members
6381+ async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError>;
6382+ }
6383+
6384+ /// ExpansionFunc wraps an async closure implementing the Expansion trait
6385+ /// # Example
6386+ /// ```rust
6387+ /// use email_address::EmailAddress;
6388+ /// use maitred::expand::ExpansionFunc;
6389+ ///
6390+ /// let my_expn_fn = ExpansionFunc(|name: &str| {
6391+ /// async move {
6392+ /// Ok(vec![
6393+ /// EmailAddress::new_unchecked("fuu@bar.com"),
6394+ /// EmailAddress::new_unchecked("baz@qux.com")
6395+ /// ])
6396+ /// }
6397+ /// });
6398+ /// ```
6399+ pub struct ExpansionFunc<F, T>(pub F)
6400+ where
6401+ F: Fn(&str) -> T + Sync + Send,
6402+ T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send;
6403+
6404+ #[async_trait]
6405+ impl<F, T> Expansion for ExpansionFunc<F, T>
6406+ where
6407+ F: Fn(&str) -> T + Sync + Send,
6408+ T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send,
6409+ {
6410+ async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError> {
6411+ let f = (self.0)(name);
6412+ f.await
6413+ }
6414+ }
6415 diff --git a/src/lib.rs b/src/lib.rs
6416new file mode 100644
6417index 0000000..fd9dd65
6418--- /dev/null
6419+++ b/src/lib.rs
6420 @@ -0,0 +1,54 @@
6421+ #![doc = include_str!("../README.md")]
6422+
6423+ pub use email_address;
6424+
6425+ #[cfg(feature = "authentication")]
6426+ pub use mail_auth;
6427+
6428+ pub use mail_parser;
6429+ pub use smtp_proto;
6430+
6431+ /// Helper module for mutating email messages
6432+ pub mod rewrite;
6433+ /// Session EXPN function helper
6434+ pub mod expand;
6435+ /// Low level SMTP session without network transport
6436+ pub mod session;
6437+ /// Session VRFY function helper
6438+ pub mod verify;
6439+ #[doc(inline)]
6440+ pub use session::{
6441+ Action, Envelope, Response, Session, DEFAULT_CAPABILITIES, DEFAULT_GREETING,
6442+ DEFAULT_HELP_BANNER, DEFAULT_MAXIMUM_MESSAGE_SIZE,
6443+ };
6444+
6445+ #[cfg(feature = "authentication")]
6446+ pub mod authentication;
6447+
6448+ /// Message deliver helper
6449+ pub mod delivery;
6450+ pub use delivery::{Delivery, DeliveryError, DeliveryFunc};
6451+
6452+ /// SMTP Authentication
6453+ pub mod plain_auth;
6454+ #[doc(inline)]
6455+ pub use plain_auth::{AuthError, PlainAuth, PlainAuthFunc};
6456+ #[cfg(feature = "server")]
6457+ /// Embedable SMTP Server module
6458+ pub mod server;
6459+
6460+ /// Crypto helpers
6461+ #[cfg(any(feature = "server", feature = "relay"))]
6462+ pub mod crypto;
6463+ /// Relaying and client functionality
6464+ #[cfg(feature = "relay")]
6465+ pub mod relay;
6466+
6467+ /// Generate a single smtp_response
6468+ macro_rules! smtp_response {
6469+ ($code:expr, $e1:expr, $e2:expr, $e3:expr, $name:expr) => {
6470+ Response::General(SmtpResponse::new($code, $e1, $e2, $e3, $name.to_string()))
6471+ };
6472+ }
6473+
6474+ pub(crate) use smtp_response;
6475 diff --git a/src/plain_auth.rs b/src/plain_auth.rs
6476new file mode 100644
6477index 0000000..9094154
6478--- /dev/null
6479+++ b/src/plain_auth.rs
6480 @@ -0,0 +1,187 @@
6481+ use std::{future::Future, string::FromUtf8Error};
6482+
6483+ use async_trait::async_trait;
6484+ use base64::{prelude::*, DecodeError};
6485+ use stringprep::{saslprep, Error as SaslPrepError};
6486+
6487+ use crate::session::Response;
6488+ use crate::smtp_response;
6489+ use smtp_proto::Response as SmtpResponse;
6490+
6491+ /// Any error that occurred during authentication.
6492+ #[derive(Debug, thiserror::Error)]
6493+ pub enum AuthError {
6494+ #[error("Unauthorized")]
6495+ Unauthorized,
6496+ #[error("Input too long, maximum 255 characters")]
6497+ InputTooLong,
6498+ #[error("Not enough fields")]
6499+ NotEnoughFields,
6500+ #[error("Failed to decode authentication data: {0}")]
6501+ Base64Decoding(#[from] DecodeError),
6502+ #[error("Bad input: {0}")]
6503+ SaslPrep(#[from] SaslPrepError),
6504+ #[error("Not valid UTF8: {0}")]
6505+ Utf8(#[from] FromUtf8Error),
6506+ }
6507+
6508+ #[allow(clippy::from_over_into)]
6509+ impl Into<Response<String>> for AuthError {
6510+ fn into(self) -> Response<String> {
6511+ let message = self.to_string();
6512+ match self {
6513+ AuthError::Unauthorized => {
6514+ smtp_response!(400, 0, 0, 0, message)
6515+ }
6516+ AuthError::InputTooLong => {
6517+ smtp_response!(500, 0, 0, 0, message)
6518+ }
6519+ AuthError::NotEnoughFields => {
6520+ smtp_response!(500, 0, 0, 0, message)
6521+ }
6522+ AuthError::Base64Decoding(err) => {
6523+ smtp_response!(500, 0, 0, 0, err.to_string())
6524+ }
6525+ AuthError::SaslPrep(err) => {
6526+ smtp_response!(500, 0, 0, 0, err.to_string())
6527+ }
6528+ AuthError::Utf8(err) => {
6529+ smtp_response!(500, 0, 0, 0, err.to_string())
6530+ }
6531+ }
6532+ }
6533+ }
6534+
6535+ /// Authentication trait for handling PLAIN SASL auth as defined in RFC4616
6536+ #[async_trait]
6537+ pub trait PlainAuth: Sync + Send {
6538+ /// authenticate is passed the plaintext authcid, authzid, and passwd
6539+ /// for the user. The implementer should return AuthError::Unauthorized
6540+ /// if the credentials are invalid.
6541+ async fn authenticate(
6542+ &self,
6543+ authcid: &str,
6544+ authzid: &str,
6545+ passwd: &str,
6546+ ) -> Result<String, AuthError>;
6547+ }
6548+
6549+ /// Convenience function implementing PlainAuth
6550+ pub struct PlainAuthFunc<F, T>(pub F)
6551+ where
6552+ F: Fn(&str, &str, &str) -> T + Sync + Send,
6553+ T: Future<Output = Result<(), AuthError>> + Send;
6554+
6555+ #[async_trait]
6556+ impl<F, T> PlainAuth for PlainAuthFunc<F, T>
6557+ where
6558+ F: Fn(&str, &str, &str) -> T + Sync + Send,
6559+ T: Future<Output = Result<(), AuthError>> + Send,
6560+ {
6561+ async fn authenticate(
6562+ &self,
6563+ authcid: &str,
6564+ authzid: &str,
6565+ passwd: &str,
6566+ ) -> Result<String, AuthError> {
6567+ let f = (self.0)(authcid, authzid, passwd);
6568+ match f.await {
6569+ Ok(_) => Ok(authcid.to_string()),
6570+ Err(e) => Err(e),
6571+ }
6572+ }
6573+ }
6574+
6575+ /// Read a PLAIN SASL mechanism per RFC4616
6576+ /// The mechanism consists of a single message, a string of [UTF-8]
6577+ /// encoded [Unicode] characters, from the client to the server. The
6578+ /// client presents the authorization identity (identity to act as),
6579+ /// followed by a NUL (U+0000) character, followed by the authentication
6580+ /// identity (identity whose password will be used), followed by a NUL
6581+ /// (U+0000) character, followed by the clear-text password.
6582+ #[derive(Default)]
6583+ pub(crate) struct AuthData {
6584+ values: [String; 3],
6585+ }
6586+
6587+ impl AuthData {
6588+ pub fn authcid(&self) -> String {
6589+ self.values[0].clone()
6590+ }
6591+
6592+ pub fn authzid(&self) -> String {
6593+ self.values[1].clone()
6594+ }
6595+
6596+ pub fn passwd(&self) -> String {
6597+ self.values[2].clone()
6598+ }
6599+ }
6600+
6601+ impl TryFrom<&str> for AuthData {
6602+ type Error = AuthError;
6603+
6604+ fn try_from(value: &str) -> Result<Self, Self::Error> {
6605+ let decoded = BASE64_STANDARD.decode(value)?;
6606+ let mut n = 0;
6607+ let mut raw_data: [Vec<u8>; 3] = [
6608+ Vec::with_capacity(255),
6609+ Vec::with_capacity(255),
6610+ Vec::with_capacity(255),
6611+ ];
6612+ for (i, ch) in decoded.iter().enumerate() {
6613+ if *ch == b'\0' {
6614+ if i > 0 {
6615+ n += 1;
6616+ }
6617+ continue;
6618+ }
6619+ if raw_data[n].len() + 1 > 255 {
6620+ return Err(AuthError::InputTooLong);
6621+ }
6622+ raw_data[n].push(*ch);
6623+ }
6624+ if n == 0 {
6625+ return Err(AuthError::NotEnoughFields);
6626+ }
6627+ if raw_data[2].is_empty() {
6628+ // if only an athcid and passwd were specified shift the value
6629+ // from authzid.
6630+ raw_data[2] = raw_data[1].clone();
6631+ raw_data[1] = raw_data[0].clone();
6632+ }
6633+ // RFC4013
6634+ let sasl_authcid = String::from_utf8(raw_data[0].to_vec())?;
6635+ let sasl_authcid = saslprep(&sasl_authcid)?;
6636+ let sasl_authzid = String::from_utf8(raw_data[1].to_vec())?;
6637+ let sasl_authzid = saslprep(&sasl_authzid)?;
6638+ let sasl_passwd = String::from_utf8(raw_data[2].to_vec())?;
6639+ let sasl_passwd = saslprep(&sasl_passwd)?;
6640+ Ok(AuthData {
6641+ values: [
6642+ sasl_authcid.to_string(),
6643+ sasl_authzid.to_string(),
6644+ sasl_passwd.to_string(),
6645+ ],
6646+ })
6647+ }
6648+ }
6649+
6650+ #[cfg(test)]
6651+ mod tests {
6652+
6653+ use super::*;
6654+ use base64::engine::general_purpose::STANDARD;
6655+
6656+ #[test]
6657+ pub fn test_auth_data() {
6658+ let data = AuthData::try_from(STANDARD.encode(b"\0hello\0world").as_str()).unwrap();
6659+ assert!(data.authcid() == "hello");
6660+ assert!(data.authzid() == "hello");
6661+ assert!(data.passwd() == "world");
6662+ let data = AuthData::try_from(STANDARD.encode(b"\0fuu\0bar\0baz").as_str()).unwrap();
6663+ assert!(data.authcid() == "fuu");
6664+ assert!(data.authzid() == "bar");
6665+ assert!(data.passwd() == "baz");
6666+ }
6667+ }
6668 diff --git a/src/relay.rs b/src/relay.rs
6669new file mode 100644
6670index 0000000..8bf3136
6671--- /dev/null
6672+++ b/src/relay.rs
6673 @@ -0,0 +1,291 @@
6674+ use std::collections::HashMap;
6675+ use std::fmt::Display;
6676+ use std::time::Duration;
6677+
6678+ use hickory_resolver::error::ResolveError;
6679+ use hickory_resolver::proto::rr::rdata::MX;
6680+ use hickory_resolver::system_conf::read_system_conf;
6681+ use hickory_resolver::TokioAsyncResolver;
6682+ use lettre::error::Error as LettreError;
6683+ use lettre::transport::smtp::client::{Tls, TlsParameters};
6684+ use lettre::transport::smtp::extension::ClientId;
6685+ use lettre::transport::smtp::Error as LettreTransportError;
6686+ use lettre::{address::AddressError, AsyncTransport};
6687+ use lettre::{AsyncSmtpTransport, Tokio1Executor};
6688+ use mail_parser::{Address, Message};
6689+
6690+ pub use lettre::address::{Address as LettreAddress, Envelope as LettreEnvelope};
6691+
6692+ const DEFAULT_SUBMISSION_PORT: u16 = 25;
6693+ const DEFAULT_TIMEOUT_MS: u64 = 5000;
6694+
6695+ /// Relay level errors
6696+ #[derive(Debug, thiserror::Error)]
6697+ pub enum Error {
6698+ #[error("Message does not contain a TO field")]
6699+ NoToAddress,
6700+ #[error("Message does not contain a FROM field")]
6701+ NoFromAddress,
6702+ #[error("Cannot parse email address: {0}")]
6703+ Address(#[from] AddressError),
6704+ #[error("Client error: {0}")]
6705+ Lettre(#[from] LettreError),
6706+ #[error("Lettre transport failures")]
6707+ LettreTransport(Vec<LettreTransportError>),
6708+ #[error("DNS Resolution: {0}")]
6709+ Resolution(#[from] ResolveError),
6710+ }
6711+
6712+ fn addresses(addr: Option<&Address<'_>>) -> Result<Vec<LettreAddress>, Error> {
6713+ Ok(addr
6714+ .map(|addr| {
6715+ addr.iter().try_fold(Vec::new(), |mut accm, addr| {
6716+ let address: LettreAddress = addr.address().unwrap().parse()?;
6717+ accm.push(address);
6718+ Ok::<Vec<LettreAddress>, Error>(accm)
6719+ })
6720+ })
6721+ .transpose()?
6722+ .unwrap_or(Vec::new()))
6723+ }
6724+
6725+ /// Sorted pairs of envelopes organized by domain
6726+ #[derive(Clone, Default)]
6727+ pub struct Sorted(pub HashMap<Hostname, LettreEnvelope>);
6728+
6729+ impl Sorted {
6730+ /// Return a sorted list of domains specified in the message with their
6731+ /// envelope for use in the Lettre transport.
6732+ pub fn from_message(message: &Message<'_>) -> Result<Self, Error> {
6733+ let from = message.from().map_or(Err(Error::NoFromAddress), |from| {
6734+ from.first().map_or(Err(Error::NoFromAddress), |from| {
6735+ let address: LettreAddress = from.address().unwrap().parse()?;
6736+ Ok(address)
6737+ })
6738+ })?;
6739+ let unsorted = [
6740+ addresses(message.to())?,
6741+ addresses(message.cc())?,
6742+ addresses(message.bcc())?, // TODO: Should BCC work the same?
6743+ ]
6744+ .concat();
6745+ let sorted: HashMap<String, Vec<LettreAddress>> =
6746+ unsorted.iter().fold(HashMap::new(), |mut accm, address| {
6747+ if let Some(addresses) = accm.get_mut(address.domain()) {
6748+ addresses.push(address.clone());
6749+ } else {
6750+ accm.insert(address.domain().to_string(), vec![address.clone()]);
6751+ }
6752+ accm
6753+ });
6754+ Ok(Sorted(sorted.iter().fold(
6755+ HashMap::new(),
6756+ |mut accm, (domain, addresses)| {
6757+ accm.insert(
6758+ Hostname::new(domain.as_ref()),
6759+ LettreEnvelope::new(Some(from.clone()), addresses.clone()).unwrap(),
6760+ );
6761+ accm
6762+ },
6763+ )))
6764+ }
6765+ }
6766+
6767+ /// RFC1123 Hostname
6768+ /// TODO: Actually implement RFC1123
6769+ #[derive(Clone, Debug, Eq, Hash, PartialEq)]
6770+ pub struct Hostname(String);
6771+
6772+ impl Hostname {
6773+ pub fn new(hostname: &str) -> Self {
6774+ Hostname(hostname.to_string())
6775+ }
6776+ }
6777+
6778+ impl From<Hostname> for String {
6779+ fn from(val: Hostname) -> Self {
6780+ val.0.clone()
6781+ }
6782+ }
6783+
6784+ impl From<&str> for Hostname {
6785+ fn from(value: &str) -> Self {
6786+ Hostname(value.to_string())
6787+ }
6788+ }
6789+
6790+ impl Display for Hostname {
6791+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
6792+ f.write_str(self.0.as_ref())
6793+ }
6794+ }
6795+
6796+ #[derive(Clone)]
6797+ pub enum TlsConfiguration {
6798+ Insecure,
6799+ Opportunistic,
6800+ }
6801+
6802+ #[derive(Default)]
6803+ pub struct RelayBuilder {
6804+ hostname: Option<ClientId>,
6805+ port: Option<u16>,
6806+ tls: Option<TlsConfiguration>,
6807+ resolve_dns: Option<bool>,
6808+ }
6809+
6810+ impl RelayBuilder {
6811+ pub fn build(&self) -> Relay {
6812+ Relay {
6813+ hostname: self
6814+ .hostname
6815+ .as_ref()
6816+ .map_or(ClientId::default(), |hostname| hostname.clone()),
6817+ port: self.port.unwrap_or(DEFAULT_SUBMISSION_PORT),
6818+ tls: match self.tls {
6819+ Some(TlsConfiguration::Insecure) => TlsConfiguration::Insecure,
6820+ Some(TlsConfiguration::Opportunistic) | None => TlsConfiguration::Opportunistic,
6821+ },
6822+ resolve_dns: self.resolve_dns.is_some_and(|resolve| resolve),
6823+ }
6824+ }
6825+
6826+ pub fn insecure(mut self) -> Self {
6827+ self.tls = Some(TlsConfiguration::Insecure);
6828+ self
6829+ }
6830+
6831+ pub fn port(mut self, port: u16) -> Self {
6832+ self.port = Some(port);
6833+ self
6834+ }
6835+
6836+ pub fn resolve_dns(mut self, enabled: bool) -> Self {
6837+ self.resolve_dns = Some(enabled);
6838+ self
6839+ }
6840+ }
6841+
6842+ /// Implements a message relay
6843+ pub struct Relay {
6844+ /// Hostname advertised in during SMTP HELO
6845+ hostname: ClientId,
6846+ port: u16,
6847+ tls: TlsConfiguration,
6848+ /// If enabled DNS resolution will be performed to lookup MX records for
6849+ /// the host. If disabled then the hostname will is assumed to be literal
6850+ /// and an SMTP connection is established.
6851+ resolve_dns: bool,
6852+ }
6853+
6854+ impl Relay {
6855+ pub fn builder() -> RelayBuilder {
6856+ RelayBuilder::default()
6857+ }
6858+
6859+ fn transport(&self, hostname: Hostname) -> Result<AsyncSmtpTransport<Tokio1Executor>, Error> {
6860+ let builder = AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname.clone())
6861+ .hello_name(ClientId::Domain(self.hostname.to_string()))
6862+ .timeout(Some(Duration::from_secs(DEFAULT_TIMEOUT_MS)))
6863+ .port(self.port)
6864+ .tls(match self.tls {
6865+ TlsConfiguration::Insecure => Tls::None,
6866+ TlsConfiguration::Opportunistic => {
6867+ Tls::Opportunistic(TlsParameters::builder(hostname.0).build_rustls().unwrap())
6868+ }
6869+ });
6870+ let transport = builder.build();
6871+ Ok(transport)
6872+ }
6873+
6874+ async fn resolve_mx_record(&self, domain: &str) -> Result<Vec<Hostname>, ResolveError> {
6875+ tracing::info!("Looking up MX records for domain: {}", domain);
6876+ let (cfg, opts) = read_system_conf()?;
6877+ let resolver = TokioAsyncResolver::tokio(cfg, opts);
6878+ let response = resolver.mx_lookup(domain).await?;
6879+ let mut records: Vec<MX> = response.iter().cloned().collect();
6880+ records.sort_by_key(|record| record.preference());
6881+ records.iter().for_each(|record| {
6882+ tracing::info!("Resolved record: {}", record.to_string());
6883+ });
6884+ Ok(records
6885+ .iter()
6886+ .map(|record| Hostname::new(&record.exchange().to_utf8())) // FIXME ?
6887+ .collect())
6888+ }
6889+
6890+ /// Send a raw message
6891+ pub async fn send(
6892+ &self,
6893+ hostname: &Hostname,
6894+ envelope: &LettreEnvelope,
6895+ message: &[u8],
6896+ ) -> Result<(), Error> {
6897+ let hostnames = if self.resolve_dns {
6898+ self.resolve_mx_record(&hostname.0).await?
6899+ } else {
6900+ vec![hostname.clone()]
6901+ };
6902+ let mut failures: Vec<LettreTransportError> = Vec::new();
6903+ for hostname in hostnames {
6904+ tracing::info!("Attempting to deliver message to mail server: {}", hostname);
6905+ let transport = self.transport(hostname.clone())?;
6906+ match transport.send_raw(envelope, message).await {
6907+ Ok(_) => return Ok(()),
6908+ Err(e) => {
6909+ tracing::warn!("Failed to relay message to mail server: {}", e);
6910+ failures.push(e);
6911+ continue;
6912+ }
6913+ }
6914+ }
6915+ Err(Error::LettreTransport(failures))
6916+ }
6917+ }
6918+
6919+ #[cfg(test)]
6920+ mod test {
6921+
6922+ use super::*;
6923+ use mail_parser::MessageParser;
6924+
6925+ const TEST_EMAIL: &str = r#"From: hello@ayllu-forge.org
6926+ To: dev@ayllu-dev.local
6927+ Cc: Fuu Bar <me@example.org>
6928+ Subject: [PATCH] add delivery parameters for mail module in db crate
6929+ Date: Mon, 23 Dec 2024 18:49:34 +0100
6930+ Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
6931+ X-Mailer: git-send-email 2.47.1
6932+ MIME-Version: 1.0
6933+ Content-Transfer-Encoding: 8bit
6934+
6935+ From: Fuu Bar <me@example.org>
6936+
6937+ ---
6938+ ayllu-mail/src/delivery.rs | 12 +++++-----
6939+
6940+ TRUNCATED
6941+ "#;
6942+
6943+ #[test]
6944+ fn sorted_parsing() {
6945+ let parser = MessageParser::new();
6946+ let message = parser.parse(TEST_EMAIL).unwrap();
6947+ let sorted = Sorted::from_message(&message).unwrap();
6948+ assert!(sorted.0.len() == 2);
6949+ let d1 = sorted.0.get(&"ayllu-dev.local".into()).unwrap();
6950+ let d1_from = d1.from().unwrap();
6951+ assert!(d1_from.user() == "hello");
6952+ assert!(d1_from.domain() == "ayllu-forge.org");
6953+ assert!(d1.to().len() == 1);
6954+ assert!(d1.to()[0].user() == "dev");
6955+ assert!(d1.to()[0].domain() == "ayllu-dev.local");
6956+ let d2 = sorted.0.get(&"example.org".into()).unwrap();
6957+ let d2_from = d2.from().unwrap();
6958+ assert!(d2_from.user() == "hello");
6959+ assert!(d2_from.domain() == "ayllu-forge.org");
6960+ assert!(d2.to().len() == 1);
6961+ assert!(d2.to()[0].user() == "me");
6962+ assert!(d2.to()[0].domain() == "example.org");
6963+ }
6964+ }
6965 diff --git a/src/rewrite.rs b/src/rewrite.rs
6966new file mode 100644
6967index 0000000..8f02ee0
6968--- /dev/null
6969+++ b/src/rewrite.rs
6970 @@ -0,0 +1,73 @@
6971+ use mail_parser::{HeaderName, Message, MessageParser};
6972+
6973+ /// Basically a hack that can modify messages expensively re-parsing them on
6974+ /// each modificaiton. The mail_parser project has mentioned adding this
6975+ /// functionality and perhaps this could be upstreamed.
6976+ pub struct Rewrite<'a> {
6977+ parser: MessageParser,
6978+ raw_message: &'a mut Vec<u8>,
6979+ }
6980+
6981+ impl<'a> Rewrite<'a> {
6982+ pub fn new(parser: Option<MessageParser>, raw_message: &'a mut Vec<u8>) -> Rewrite<'a> {
6983+ Self {
6984+ parser: parser.unwrap_or_default(),
6985+ raw_message,
6986+ }
6987+ }
6988+
6989+ pub fn message(&'a self) -> Message<'a> {
6990+ self.parser
6991+ .parse(self.raw_message.as_slice())
6992+ .expect("Cannot parse message")
6993+ }
6994+
6995+ /// Set the header to a string value replacing it if it already exists
6996+ pub fn set_header(&mut self, key: HeaderName, value: &str) {
6997+ let message = self
6998+ .parser
6999+ .parse_headers(self.raw_message.as_slice())
7000+ .expect("Cannot parse message");
7001+ if let Some(header) = message.headers().iter().find(|header| header.name() == key.as_str()) {
7002+ let (start, end) = (header.offset_field(), header.offset_end());
7003+ self.raw_message.drain(start..end);
7004+ }
7005+ let header: Vec<u8> = format!("{}: {}\n", key, value.trim_end()).bytes().collect();
7006+ self.raw_message.splice(0..0, header);
7007+ }
7008+ }
7009+
7010+ #[cfg(test)]
7011+ mod test {
7012+ use super::*;
7013+
7014+ const TEST_EMAIL: &str = r#"Date: Mon, 2 Sep 2024 00:17:18 +0200
7015+ From: kevin <kevin@ayllu-dev.local>
7016+ To: hello@example.org
7017+ Subject: Fuu
7018+ Message-ID: <ewo47gsen3mimmdzlg5v4otplgiwwyogq7avzbs26lxnir3rem@xgg5xxzon6f7>
7019+ MIME-Version: 1.0
7020+ Content-Type: text/plain; charset=us-ascii
7021+ Content-Disposition: inline
7022+
7023+ Hello World
7024+ "#;
7025+
7026+ #[test]
7027+ fn rewrite() {
7028+ let email_bytes = &mut TEST_EMAIL.as_bytes().to_vec();
7029+ let mut rewrite = Rewrite::new(None, email_bytes);
7030+ rewrite.set_header(HeaderName::Other("a".into()), "b");
7031+ rewrite.set_header(HeaderName::Subject, "Bar");
7032+ let message = rewrite.message();
7033+ println!("{}", String::from_utf8_lossy(message.raw_message()));
7034+ let value = message.header("a").unwrap();
7035+ assert!(value.as_text().unwrap() == "b");
7036+ let value = message.header("Subject").unwrap();
7037+ assert!(value.as_text().unwrap() == "Bar");
7038+ let message_str = String::from_utf8(message.raw_message().to_vec()).unwrap();
7039+ assert!(message_str.split("\n").next().unwrap() == "Subject: Bar");
7040+ assert!(message_str.split("\n").nth(1).unwrap() == "a: b");
7041+ assert!(message_str.split("\n").nth(2).unwrap() == "Date: Mon, 2 Sep 2024 00:17:18 +0200");
7042+ }
7043+ }
7044 diff --git a/src/server/mod.rs b/src/server/mod.rs
7045new file mode 100644
7046index 0000000..2ada76f
7047--- /dev/null
7048+++ b/src/server/mod.rs
7049 @@ -0,0 +1,627 @@
7050+ mod opportunistic;
7051+ mod transport;
7052+
7053+ use std::fs::File as StdFile;
7054+ use std::io::BufReader as StdBufReader;
7055+ use std::net::{IpAddr, SocketAddr};
7056+ use std::path::{Path, PathBuf};
7057+ use std::sync::Arc;
7058+ use std::time::Duration;
7059+
7060+ use futures::SinkExt;
7061+ use proxy_header::{ParseConfig, ProxyHeader};
7062+ use smtp_proto::Response as SmtpResponse;
7063+ use tokio::net::TcpListener;
7064+ use tokio::sync::Mutex;
7065+ use tokio::time::timeout;
7066+ use tokio_rustls::{rustls, TlsAcceptor};
7067+ use tokio_util::codec::Framed;
7068+
7069+ use crate::plain_auth::PlainAuth;
7070+ use crate::delivery::Delivery;
7071+ use crate::expand::Expansion;
7072+ use crate::session::{Response, Session};
7073+ use crate::smtp_response;
7074+ use crate::verify::Verify;
7075+ use opportunistic::{Opportunistic, Plain, Tls};
7076+ use transport::{Command, Transport, TransportError};
7077+
7078+ /// The default port the server will listen on if none was specified in it's
7079+ /// configuration options.
7080+ pub const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:2525";
7081+
7082+ /// Maximum amount of time the server will wait for a command before closing
7083+ /// the connection.
7084+ pub const DEFAULT_GLOBAL_TIMEOUT_SECS: u64 = 300;
7085+
7086+ /// Top level error encountered while processing a client connection, causes
7087+ /// a warning to be logged but is not fatal.
7088+ #[derive(Debug, thiserror::Error)]
7089+ pub enum ServerError {
7090+ /// An IO related error such as not being able to bind to a TCP socket
7091+ #[error("Io: {0}")]
7092+ Io(#[from] std::io::Error),
7093+ #[error("Transport Error: {0}")]
7094+ Transport(#[from] TransportError),
7095+ /// Session timeout
7096+ #[error("Client took too long to respond: {0}s")]
7097+ Timeout(u64),
7098+ #[error("Failed to configure TLS: {0}")]
7099+ TlsConfiguration(#[from] rustls::Error),
7100+ #[error("Proxy Protocol Error: {0}")]
7101+ ProxyProtocol(#[from] proxy_header::Error),
7102+ }
7103+
7104+ /// Action for controlling a TCP session
7105+ pub(crate) enum Action {
7106+ Continue,
7107+ Shutdown,
7108+ TlsUpgrade,
7109+ }
7110+
7111+ /// Server implements everything that is required to run an SMTP server by
7112+ /// binding to the configured address and processing individual TCP connections
7113+ /// as they are received.
7114+ /// ```rust,no_run
7115+ #[doc = include_str!("../../examples/server.rs")]
7116+ /// ```
7117+ pub struct Server {
7118+ address: String,
7119+ our_hostname: String,
7120+ global_timeout: Duration,
7121+ pipelining: bool,
7122+ delivery: Option<Arc<dyn Delivery>>,
7123+ list_expansion: Option<Arc<dyn Expansion>>,
7124+ verification: Option<Arc<dyn Verify>>,
7125+ plain_auth: Option<Arc<dyn PlainAuth>>,
7126+ tls_certificates: Option<(PathBuf, PathBuf)>,
7127+ proxy_protocol: bool,
7128+ session: Session,
7129+ }
7130+
7131+ impl Default for Server {
7132+ fn default() -> Self {
7133+ Server {
7134+ address: DEFAULT_LISTEN_ADDR.to_string(),
7135+ our_hostname: String::default(),
7136+ global_timeout: Duration::from_secs(DEFAULT_GLOBAL_TIMEOUT_SECS),
7137+ pipelining: true,
7138+ delivery: None,
7139+ list_expansion: None,
7140+ plain_auth: None,
7141+ verification: None,
7142+ tls_certificates: None,
7143+ proxy_protocol: false,
7144+ session: Session::default(),
7145+ }
7146+ }
7147+ }
7148+
7149+ impl Server {
7150+ /// Listener address for the SMTP server to bind to listen for incoming
7151+ /// connections.
7152+ pub fn address(mut self, address: &str) -> Self {
7153+ self.address = address.to_string();
7154+ self
7155+ }
7156+
7157+ /// The hostname of this server
7158+ pub fn our_hostname(mut self, hostname: &str) -> Self {
7159+ self.our_hostname = hostname.to_string();
7160+ self
7161+ }
7162+
7163+ /// Set the maximum amount of time the server will wait for another command
7164+ /// before closing the connection. RFC states the suggested time is 5m.
7165+ pub fn timeout(mut self, timeout: Duration) -> Self {
7166+ self.global_timeout = timeout;
7167+ self
7168+ }
7169+
7170+ /// If piplining is supported in the transport, typically should be yes
7171+ /// but the session could explicitly disable it.
7172+ pub fn pipelining(mut self, enabled: bool) -> Self {
7173+ self.pipelining = enabled;
7174+ self
7175+ }
7176+
7177+ /// Delivery handles the delivery of the final message
7178+ pub fn with_delivery<T>(mut self, delivery: T) -> Self
7179+ where
7180+ T: Delivery + 'static,
7181+ {
7182+ self.delivery = Some(Arc::new(delivery));
7183+ self
7184+ }
7185+
7186+ pub fn list_expansion<T>(mut self, expansion: T) -> Self
7187+ where
7188+ T: crate::expand::Expansion + 'static,
7189+ {
7190+ self.list_expansion = Some(Arc::new(expansion));
7191+ self
7192+ }
7193+
7194+ pub fn verification<T>(mut self, verification: T) -> Self
7195+ where
7196+ T: crate::verify::Verify + 'static,
7197+ {
7198+ self.verification = Some(Arc::new(verification));
7199+ self
7200+ }
7201+
7202+ pub fn plain_auth<T>(mut self, plain_auth: T) -> Self
7203+ where
7204+ T: crate::plain_auth::PlainAuth + 'static,
7205+ {
7206+ self.plain_auth = Some(Arc::new(plain_auth));
7207+ self
7208+ }
7209+
7210+ /// TLS Certificates, implies that the server should listen for TLS
7211+ /// connections and maybe support STARTTLS if configured in the Session
7212+ /// options.
7213+ pub fn with_certificates(mut self, private_key: &Path, certificate: &Path) -> Self {
7214+ self.tls_certificates = Some((private_key.to_path_buf(), certificate.to_path_buf()));
7215+ self
7216+ }
7217+
7218+ /// Enable support for HAProxy's
7219+ /// [Proxy Protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt)
7220+ pub fn proxy_protocol(mut self, enabled: bool) -> Self {
7221+ self.proxy_protocol = enabled;
7222+ self
7223+ }
7224+
7225+ async fn rustls_config(&self) -> Result<rustls::ServerConfig, ServerError> {
7226+ let (private_key_path, cert_path) = self
7227+ .tls_certificates
7228+ .as_ref()
7229+ .expect("Certificates not configured");
7230+ let mut cert_contents = StdBufReader::new(StdFile::open(cert_path)?);
7231+ let mut private_key_contents = StdBufReader::new(StdFile::open(private_key_path)?);
7232+ let certs = rustls_pemfile::certs(&mut cert_contents).collect::<Result<Vec<_>, _>>()?;
7233+ let private_key = rustls_pemfile::private_key(&mut private_key_contents)?.unwrap();
7234+ Ok(rustls::ServerConfig::builder()
7235+ .with_no_client_auth()
7236+ .with_single_cert(certs, private_key)?)
7237+ }
7238+
7239+ /// drive the session forward
7240+ async fn next(
7241+ &self,
7242+ client_ip: IpAddr,
7243+ conn: impl Opportunistic,
7244+ session: &mut Session,
7245+ ) -> Result<Action, ServerError> {
7246+ match timeout(self.global_timeout, conn.next()).await {
7247+ Ok(Some(Ok(Command::Requests(requests)))) => {
7248+ for request in requests {
7249+ let action = session.next(Some(&request));
7250+ match action {
7251+ crate::session::Action::Send(response) => {
7252+ conn.send(response).await?;
7253+ }
7254+ crate::session::Action::SendMany(responses) => {
7255+ for response in responses {
7256+ conn.send(response).await?;
7257+ }
7258+ }
7259+ crate::session::Action::Message {
7260+ initial_response,
7261+ cb,
7262+ } => {
7263+ conn.send(initial_response).await?;
7264+ match conn.next().await {
7265+ Some(Ok(Command::Payload(payload))) => match cb.call(client_ip, &payload) {
7266+ crate::session::Action::Send(response) => {
7267+ conn.send(response).await?;
7268+ }
7269+ crate::session::Action::Envelope {
7270+ initial_response,
7271+ envelope,
7272+ } => {
7273+ conn.send(initial_response).await?;
7274+ if let Some(delivery) = self.delivery.as_ref() {
7275+ match delivery.deliver(&envelope).await {
7276+ Ok(_) => {
7277+ conn.send(smtp_response!(200, 0, 0, 0, "OK"))
7278+ .await?
7279+ }
7280+ Err(e) => {
7281+ tracing::warn!("Delivery failure: {}", e);
7282+ conn.send(e.into()).await?
7283+ }
7284+ }
7285+ }
7286+ return Ok(Action::Continue);
7287+ }
7288+ _ => unreachable!(),
7289+ },
7290+ _ => unreachable!(),
7291+ }
7292+ }
7293+ crate::session::Action::PlainAuth {
7294+ authcid,
7295+ authzid,
7296+ password,
7297+ cb,
7298+ } => {
7299+ let plain_auth = self
7300+ .plain_auth
7301+ .as_ref()
7302+ .expect("authentication not available");
7303+ match cb
7304+ .call(plain_auth.authenticate(&authcid, &authzid, &password).await)
7305+ {
7306+ crate::session::Action::Send(response) => {
7307+ conn.send(response).await?;
7308+ }
7309+ _ => unreachable!(),
7310+ }
7311+ }
7312+ crate::session::Action::Verify { address, cb } => {
7313+ let verification = self
7314+ .verification
7315+ .as_ref()
7316+ .expect("verification not available");
7317+ match cb.call(verification.verify(&address).await) {
7318+ crate::session::Action::Send(response) => {
7319+ conn.send(response).await?;
7320+ }
7321+ _ => unreachable!(),
7322+ }
7323+ }
7324+ crate::session::Action::Expand { address, cb } => {
7325+ let expansion = self
7326+ .list_expansion
7327+ .as_ref()
7328+ .expect("expansion not available");
7329+ match cb.call(expansion.expand(&address).await) {
7330+ crate::session::Action::Send(response) => {
7331+ conn.send(response).await?;
7332+ }
7333+ _ => unreachable!(),
7334+ }
7335+ }
7336+ crate::session::Action::StartTls(response) => {
7337+ // Go ahead
7338+ conn.send(response).await?;
7339+ return Ok(Action::TlsUpgrade);
7340+ }
7341+ crate::session::Action::Quit(response) => {
7342+ conn.send(response).await?;
7343+ return Ok(Action::Shutdown);
7344+ }
7345+ crate::session::Action::Envelope {
7346+ initial_response: _,
7347+ envelope: _,
7348+ } => unreachable!(),
7349+ }
7350+ }
7351+ Ok(Action::Continue)
7352+ }
7353+ Ok(Some(Ok(Command::Payload(_)))) => unreachable!(),
7354+ Ok(Some(Err(err))) => {
7355+ tracing::warn!("Transport Error: {}", err);
7356+ let response = match err {
7357+ TransportError::PipelineNotEnabled => {
7358+ crate::smtp_response!(500, 0, 0, 0, "Pipelining is not enabled")
7359+ }
7360+ TransportError::Smtp(e) => crate::session::smtp_error_to_response(e),
7361+ // IO Errors considered fatal for the entire session
7362+ TransportError::Io(e) => return Err(ServerError::Io(e)),
7363+ };
7364+ conn.send(response).await?;
7365+ Ok(Action::Continue)
7366+ }
7367+ Ok(None) => Ok(Action::Shutdown),
7368+ Err(elapsed) => {
7369+ tracing::warn!("Client timeout: {}", elapsed);
7370+ conn.send(crate::session::timeout(&elapsed.to_string()))
7371+ .await?;
7372+ Err(ServerError::Timeout(self.global_timeout.as_secs()))
7373+ }
7374+ }
7375+ }
7376+
7377+ async fn serve_plain<T>(
7378+ &self,
7379+ stream: &mut T,
7380+ remote_addr: SocketAddr,
7381+ ) -> Result<(), ServerError>
7382+ where
7383+ T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
7384+ {
7385+ let mut session = self
7386+ .session
7387+ .clone()
7388+ .our_hostname(&self.our_hostname)
7389+ .starttls(self.tls_certificates.is_some())
7390+ .vrfy_enabled(self.verification.is_some())
7391+ .expn_enabled(self.list_expansion.is_some());
7392+
7393+ let mut framed = Framed::new(
7394+ &mut *stream,
7395+ Transport::default().pipelining(self.pipelining),
7396+ );
7397+
7398+ // initialize the connection with a greeting
7399+ match session.next(None) {
7400+ crate::session::Action::Send(response) => {
7401+ framed.send(response).await?;
7402+ }
7403+ _ => unreachable!(),
7404+ }
7405+
7406+ let framed = Arc::new(Mutex::new(framed));
7407+
7408+ loop {
7409+ match self
7410+ .next(
7411+ remote_addr.ip(),
7412+ Plain {
7413+ inner: framed.clone(),
7414+ },
7415+ &mut session,
7416+ )
7417+ .await?
7418+ {
7419+ Action::Continue => {}
7420+ Action::Shutdown => return Ok(()),
7421+ Action::TlsUpgrade => {
7422+ let acceptor = TlsAcceptor::from(Arc::new(self.rustls_config().await?));
7423+ let tls_stream = acceptor.accept(&mut *stream).await?;
7424+ let tls_framed =
7425+ Framed::new(tls_stream, Transport::default().pipelining(self.pipelining));
7426+ let tls_framed = Arc::new(Mutex::new(tls_framed));
7427+ // Per the RFC after TLS is established the session is
7428+ // reset.
7429+ let mut session = session.clone().tls_active(true);
7430+ loop {
7431+ match self
7432+ .next(
7433+ remote_addr.ip(),
7434+ Tls {
7435+ inner: tls_framed.clone(),
7436+ },
7437+ &mut session,
7438+ )
7439+ .await?
7440+ {
7441+ Action::Continue => {}
7442+ Action::Shutdown => return Ok(()),
7443+ Action::TlsUpgrade => unreachable!(),
7444+ }
7445+ }
7446+ }
7447+ }
7448+ }
7449+ }
7450+
7451+ pub async fn listen(&mut self) -> Result<(), ServerError> {
7452+ let listener = TcpListener::bind(&self.address).await?;
7453+ tracing::info!("Mail server listening @ {}", self.address);
7454+ loop {
7455+ let (mut socket, addr) = listener.accept().await.unwrap();
7456+ let local_addr = socket.local_addr()?;
7457+ tracing::info!("Accepted connection on: {:?} from: {:?}", local_addr, addr);
7458+ // pass the proxied address if proxy protocol is enabled
7459+ let addr = if self.proxy_protocol {
7460+ let mut buf: [u8; 512] = [0; 512];
7461+ socket.peek(&mut buf).await?;
7462+ let (header, len) = ProxyHeader::parse(&buf, ParseConfig::default())?;
7463+ tracing::info!("Parsed proxy protocol header: {:?} bytes={}", header, len);
7464+ if let Some(proxied) = header.proxied_address() {
7465+ // discard the proxy header
7466+ let mut buf = vec![0; len];
7467+ socket.readable().await?;
7468+ socket.try_read(&mut buf)?;
7469+ proxied.source
7470+ // socket.proxied.source
7471+ } else {
7472+ tracing::error!("Failed to parse proxied address");
7473+ addr
7474+ }
7475+ } else {
7476+ addr
7477+ };
7478+ match self.serve_plain(&mut socket, addr).await {
7479+ Ok(_) => {
7480+ tracing::info!("Client connection finished normally");
7481+ }
7482+ Err(err) => {
7483+ tracing::warn!("Client encountered an error: {:?}", err);
7484+ }
7485+ }
7486+ }
7487+ }
7488+ }
7489+
7490+ #[cfg(test)]
7491+ mod test {
7492+
7493+ use crate::{DeliveryFunc, Envelope};
7494+
7495+ use super::*;
7496+
7497+ use std::io;
7498+ use std::net::{Ipv4Addr, SocketAddrV4};
7499+ use std::pin::Pin;
7500+ use std::task::{Context, Poll};
7501+
7502+ use lettre::{AsyncSmtpTransport, AsyncTransport, Message as LettreMessage, Tokio1Executor};
7503+ use port_check::free_local_ipv4_port;
7504+ use tokio::io::{AsyncRead, AsyncWrite};
7505+ use tokio::sync::mpsc::channel;
7506+
7507+ /// Fake TCP stream for testing purposes with "framed" line oriented
7508+ /// requests to feed to the session processor.
7509+ #[derive(Default)]
7510+ struct FakeStream {
7511+ buffer: Vec<Vec<u8>>,
7512+ chunk: usize,
7513+ }
7514+
7515+ impl AsyncRead for FakeStream {
7516+ fn poll_read(
7517+ self: Pin<&mut Self>,
7518+ _cx: &mut Context<'_>,
7519+ buf: &mut tokio::io::ReadBuf<'_>,
7520+ ) -> Poll<io::Result<()>> {
7521+ let inner = self.get_mut();
7522+ let index = inner.chunk;
7523+ if let Some(chunk) = inner.buffer.get(index) {
7524+ inner.chunk = index + 1;
7525+ println!("Client wrote: {:?}", String::from_utf8_lossy(chunk));
7526+ buf.put_slice(chunk.as_slice());
7527+ std::task::Poll::Ready(Ok(()))
7528+ } else {
7529+ Poll::Ready(Ok(()))
7530+ }
7531+ }
7532+ }
7533+
7534+ impl AsyncWrite for FakeStream {
7535+ fn poll_write(
7536+ self: Pin<&mut Self>,
7537+ _cx: &mut Context<'_>,
7538+ buf: &[u8],
7539+ ) -> Poll<Result<usize, io::Error>> {
7540+ println!("Server responded: {:?}", String::from_utf8_lossy(buf));
7541+ Poll::Ready(Ok(buf.len()))
7542+ }
7543+
7544+ fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), io::Error>> {
7545+ Poll::Ready(Ok(()))
7546+ }
7547+
7548+ fn poll_shutdown(
7549+ self: Pin<&mut Self>,
7550+ _cx: &mut Context<'_>,
7551+ ) -> Poll<Result<(), io::Error>> {
7552+ todo!()
7553+ }
7554+ }
7555+
7556+ #[tokio::test]
7557+ async fn server() {
7558+ let (tx, mut rx) = channel::<Envelope>(1);
7559+ tokio::task::spawn(async move {
7560+ let mut stream = FakeStream {
7561+ buffer: vec![
7562+ "HELO example.org\r\n".into(),
7563+ "MAIL FROM: <fuu@bar.com>\r\n".into(),
7564+ "RCPT TO: <baz@qux.com>\r\n".into(),
7565+ "DATA\r\n".into(),
7566+ "Subject: Hello World\r\n.\r\n".into(),
7567+ "QUIT\r\n".into(),
7568+ ],
7569+ ..Default::default()
7570+ };
7571+ let server =
7572+ Server::default().with_delivery(DeliveryFunc(move |envelope: &Envelope| {
7573+ let env = envelope.clone();
7574+ let tx = tx.clone();
7575+ async move {
7576+ tx.send(env).await.unwrap();
7577+ Ok(())
7578+ }
7579+ }));
7580+ // turn off all extended capabilities
7581+ server
7582+ .serve_plain(
7583+ &mut stream,
7584+ SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)),
7585+ )
7586+ .await
7587+ .unwrap();
7588+ });
7589+ let envelope = rx.recv().await.unwrap();
7590+ assert!(envelope.mail_from.email() == "fuu@bar.com");
7591+ assert!(envelope
7592+ .rcpt_to
7593+ .first()
7594+ .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"));
7595+ }
7596+
7597+ #[tokio::test]
7598+ async fn server_pipelined() {
7599+ let (tx, mut rx) = channel::<Envelope>(1);
7600+ tokio::task::spawn(async move {
7601+ let mut stream = FakeStream {
7602+ buffer: vec![
7603+ "HELO example.org\r\n".into(),
7604+ "MAIL FROM: <fuu@bar.com>\r\n".into(),
7605+ "RCPT TO: <baz@qux.com>\r\n".into(),
7606+ "DATA\r\n".into(),
7607+ "Subject: Hello World\r\n.\r\n".into(),
7608+ "QUIT\r\n".into(),
7609+ ],
7610+ ..Default::default()
7611+ };
7612+ let server =
7613+ Server::default().with_delivery(DeliveryFunc(move |envelope: &Envelope| {
7614+ let env = envelope.clone();
7615+ let tx = tx.clone();
7616+ async move {
7617+ tx.send(env).await.unwrap();
7618+ Ok(())
7619+ }
7620+ }));
7621+ server
7622+ .serve_plain(
7623+ &mut stream,
7624+ SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(127, 0, 0, 1), 25)),
7625+ )
7626+ .await
7627+ .unwrap();
7628+ });
7629+ let packet = rx.recv().await.unwrap();
7630+ assert!(packet.mail_from.email() == "fuu@bar.com");
7631+ assert!(packet
7632+ .rcpt_to
7633+ .first()
7634+ .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"));
7635+ }
7636+
7637+ #[tokio::test]
7638+ async fn server_lettre_client() {
7639+ let test_port = free_local_ipv4_port().unwrap();
7640+ let (tx, mut rx) = channel::<bool>(1);
7641+ tokio::task::spawn(async move {
7642+ let mut server = Server::default()
7643+ .address(&format!("127.0.0.1:{}", test_port))
7644+ .with_delivery(DeliveryFunc(move |envelope: &Envelope| {
7645+ let tx_clone = tx.clone();
7646+ let matches = envelope
7647+ .body
7648+ .body_text(0)
7649+ .is_some_and(|body| body.contains("Hello World!"));
7650+ async move {
7651+ tx_clone.send(matches).await.unwrap();
7652+ Ok(())
7653+ }
7654+ }));
7655+ server.listen().await.unwrap();
7656+ });
7657+ let transport: AsyncSmtpTransport<Tokio1Executor> =
7658+ AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous("127.0.0.1")
7659+ .port(test_port)
7660+ .build();
7661+ let message = LettreMessage::builder()
7662+ .to("hello@example.org".parse().unwrap())
7663+ .from("fuu@example.org".parse().unwrap())
7664+ .cc("bar@example.org".parse().unwrap())
7665+ .body(String::from("Hello World!\n"))
7666+ .unwrap();
7667+ // FIXME: Need synchronization in the server to tell us when it's
7668+ // accepting connections.
7669+ tokio::time::sleep(Duration::from_millis(500)).await;
7670+ // BUG: Either the client doesn't respect batching or the server doesn't
7671+ // implement it correctly (probably the latter).
7672+ // assert!(transport.test_connection().await.is_ok_and(|ready| ready));
7673+ transport.send(message).await.unwrap();
7674+ assert!(rx.recv().await.is_some_and(|matches| matches));
7675+ }
7676+ }
7677 diff --git a/src/server/opportunistic.rs b/src/server/opportunistic.rs
7678new file mode 100644
7679index 0000000..eac9503
7680--- /dev/null
7681+++ b/src/server/opportunistic.rs
7682 @@ -0,0 +1,62 @@
7683+ use std::sync::Arc;
7684+
7685+ use futures::SinkExt;
7686+ use futures::StreamExt;
7687+ use tokio::sync::Mutex;
7688+ use tokio_rustls::server::TlsStream;
7689+ use tokio_util::codec::Framed;
7690+
7691+ use crate::session::Response;
7692+ use super::transport::{Command, Transport, TransportError};
7693+
7694+ /// Connection that is either over plain text or TLS
7695+ pub(crate) trait Opportunistic {
7696+ async fn send(&self, message: Response<String>) -> Result<(), TransportError>;
7697+ async fn next(&self) -> Option<Result<Command, TransportError>>;
7698+ }
7699+
7700+ /// Framed SMTP Transport over Plain Text
7701+ pub(crate) struct Plain<'a, T>
7702+ where
7703+ T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
7704+ {
7705+ pub inner: Arc<Mutex<Framed<&'a mut T, Transport>>>,
7706+ }
7707+
7708+ impl<'a, T> Opportunistic for Plain<'a, T>
7709+ where
7710+ T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
7711+ {
7712+ async fn send(&self, message: Response<String>) -> Result<(), TransportError> {
7713+ let mut inner = self.inner.lock().await;
7714+ inner.send(message).await
7715+ }
7716+
7717+ async fn next(&self) -> Option<Result<Command, TransportError>> {
7718+ let mut inner = self.inner.lock().await;
7719+ inner.next().await
7720+ }
7721+ }
7722+
7723+ /// Framed SMTP Transport over TLS
7724+ pub(crate) struct Tls<'a, T>
7725+ where
7726+ T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
7727+ {
7728+ pub inner: Arc<Mutex<Framed<TlsStream<&'a mut T>, Transport>>>,
7729+ }
7730+
7731+ impl<'a, T> Opportunistic for Tls<'a, T>
7732+ where
7733+ T: tokio::io::AsyncRead + tokio::io::AsyncWrite + std::marker::Unpin,
7734+ {
7735+ async fn send(&self, message: Response<String>) -> Result<(), TransportError> {
7736+ let mut inner = self.inner.lock().await;
7737+ inner.send(message).await
7738+ }
7739+
7740+ async fn next(&self) -> Option<Result<Command, TransportError>> {
7741+ let mut inner = self.inner.lock().await;
7742+ inner.next().await
7743+ }
7744+ }
7745 diff --git a/src/server/transport.rs b/src/server/transport.rs
7746new file mode 100644
7747index 0000000..ae847f6
7748--- /dev/null
7749+++ b/src/server/transport.rs
7750 @@ -0,0 +1,270 @@
7751+ use std::{fmt::Display, io::Write};
7752+
7753+ use bytes::{Bytes, BytesMut};
7754+ use smtp_proto::request::receiver::{BdatReceiver, DataReceiver, RequestReceiver};
7755+ use smtp_proto::Error as SmtpError;
7756+ use smtp_proto::Request;
7757+ use tokio_util::codec::{Decoder, Encoder};
7758+
7759+ use crate::session::Response;
7760+
7761+ /// Error that occurred at the transport layer
7762+ #[derive(Debug, thiserror::Error)]
7763+ pub enum TransportError {
7764+ /// Returned when a client attempts to send multiple commands sequentially
7765+ /// to the server without waiting for a response but piplining isn't
7766+ /// enabled.
7767+ #[error("Pipelining is not enabled")]
7768+ PipelineNotEnabled,
7769+ /// An error generated from the underlying SMTP protocol
7770+ #[error("Smtp failure: {0}")]
7771+ Smtp(#[from] SmtpError),
7772+ /// An IO related error such as not being able to bind to a TCP socket
7773+ #[error("Io: {0}")]
7774+ Io(#[from] std::io::Error),
7775+ }
7776+
7777+ struct Wrapper<'a>(&'a mut BytesMut);
7778+
7779+ impl Write for Wrapper<'_> {
7780+ fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
7781+ self.0.extend_from_slice(buf);
7782+ Ok(buf.len())
7783+ }
7784+
7785+ fn flush(&mut self) -> std::io::Result<()> {
7786+ Ok(())
7787+ }
7788+ }
7789+
7790+ pub(crate) enum Receiver {
7791+ Data(DataReceiver),
7792+ Bdat(BdatReceiver),
7793+ }
7794+
7795+ /// Command from the client with an optional attached payload.
7796+ #[derive(Debug)]
7797+ pub enum Command {
7798+ /// One or more requests depending if PIPELINING is enabled.
7799+ Requests(Vec<Request<String>>),
7800+ /// Message payload possibily sent over multiple frames.
7801+ Payload(Bytes),
7802+ }
7803+
7804+ impl Display for Command {
7805+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
7806+ match self {
7807+ Command::Requests(requests) => write!(f, "{:?}", requests),
7808+ Command::Payload(payload) => write!(f, "Bytes ({})", payload.len()),
7809+ }
7810+ }
7811+ }
7812+
7813+ /// Low-level line oriented transport for handling SMTP connections.
7814+ // TODO: BINARYMIME
7815+ #[derive(Default)]
7816+ pub struct Transport {
7817+ receiver: Option<Box<Receiver>>,
7818+ buf: Vec<u8>,
7819+ pipelining: bool,
7820+ }
7821+
7822+ impl Clone for Transport {
7823+ fn clone(&self) -> Self {
7824+ Transport {
7825+ receiver: None,
7826+ buf: Vec::new(),
7827+ pipelining: self.pipelining,
7828+ }
7829+ }
7830+ }
7831+
7832+ impl Transport {
7833+ /// If the transport should allow piplining commands
7834+ pub fn pipelining(mut self, enabled: bool) -> Self {
7835+ self.pipelining = enabled;
7836+ self
7837+ }
7838+ }
7839+
7840+ impl Encoder<Response<String>> for Transport {
7841+ type Error = TransportError;
7842+
7843+ fn encode(&mut self, item: Response<String>, dst: &mut BytesMut) -> Result<(), Self::Error> {
7844+ tracing::debug!("Writing response: {:?}", item);
7845+ match item {
7846+ Response::General(item) => {
7847+ item.write(Wrapper(dst))?;
7848+ }
7849+ Response::Ehlo(item) => {
7850+ item.write(Wrapper(dst))?;
7851+ }
7852+ }
7853+ Ok(())
7854+ }
7855+ }
7856+
7857+ impl Decoder for Transport {
7858+ type Item = Command;
7859+ type Error = TransportError;
7860+
7861+ fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
7862+ tracing::trace!("Decoding:\n{}", String::from_utf8_lossy(src));
7863+
7864+ if src.is_empty() {
7865+ tracing::debug!("Empty command received");
7866+ return Ok(None);
7867+ }
7868+
7869+ if let Some(rec) = self.receiver.as_mut() {
7870+ let chunk_size = src.len();
7871+ tracing::debug!("Reading {} bytes of data stream", chunk_size);
7872+ match rec.as_mut() {
7873+ Receiver::Data(data_receiver) => {
7874+ let chunk = src.split_to(src.len());
7875+ if data_receiver.ingest(&mut chunk.iter(), &mut self.buf) {
7876+ tracing::debug!("Finished parsing data stream");
7877+ let payload = Bytes::copy_from_slice(&self.buf);
7878+ self.buf.clear();
7879+ self.receiver = None;
7880+ return Ok(Some(Command::Payload(payload)));
7881+ } else {
7882+ return Ok(None);
7883+ }
7884+ }
7885+ Receiver::Bdat(bdat_receiver) => {
7886+ let chunk = src.split_to(src.len());
7887+ if bdat_receiver.ingest(&mut chunk.iter(), &mut self.buf) {
7888+ tracing::debug!("Finished parsing data stream");
7889+ let payload = Bytes::copy_from_slice(&self.buf);
7890+ self.buf.clear();
7891+ self.receiver = None;
7892+ return Ok(Some(Command::Payload(payload)));
7893+ } else {
7894+ return Ok(None);
7895+ }
7896+ }
7897+ }
7898+ };
7899+
7900+ let mut r = RequestReceiver::default();
7901+ let mut requests: Vec<Request<String>> = Vec::new();
7902+ let mut iter = src.iter();
7903+ 'outer: loop {
7904+ match r.ingest(&mut iter, src) {
7905+ Ok(request) => {
7906+ if !requests.is_empty() && !self.pipelining {
7907+ return Err(TransportError::PipelineNotEnabled);
7908+ }
7909+ requests.push(request);
7910+ }
7911+ Err(err) => {
7912+ if matches!(err, smtp_proto::Error::NeedsMoreData { bytes_left: _ }) {
7913+ break 'outer;
7914+ } else {
7915+ return Err(TransportError::Smtp(err));
7916+ }
7917+ }
7918+ }
7919+ }
7920+
7921+ src.clear();
7922+
7923+ let last = requests.last().expect("No data parsed");
7924+ match last {
7925+ Request::Bdat {
7926+ chunk_size,
7927+ is_last,
7928+ } => {
7929+ tracing::info!("Starting binary data transfer");
7930+ self.receiver = Some(Box::new(Receiver::Bdat(BdatReceiver::new(
7931+ *chunk_size,
7932+ *is_last,
7933+ ))));
7934+ self.buf.clear();
7935+ Ok(Some(Command::Requests(requests)))
7936+ }
7937+ Request::Data => {
7938+ tracing::info!("Starting data transfer");
7939+ self.receiver = Some(Box::new(Receiver::Data(DataReceiver::new())));
7940+ self.buf.clear();
7941+ Ok(Some(Command::Requests(requests)))
7942+ }
7943+ _ => Ok(Some(Command::Requests(requests))),
7944+ }
7945+ }
7946+ }
7947+
7948+ #[cfg(test)]
7949+ mod test {
7950+
7951+ use super::*;
7952+
7953+ use bytes::BytesMut;
7954+
7955+ #[test]
7956+ fn sequential_commands() {
7957+ let mut transport = Transport::default();
7958+ match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
7959+ Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
7960+ _ => panic!(),
7961+ };
7962+ match transport.decode(&mut BytesMut::from("DATA\r\n")) {
7963+ Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
7964+ _ => panic!(),
7965+ };
7966+ match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
7967+ Ok(None) => {}
7968+ _ => panic!(),
7969+ };
7970+ match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
7971+ Ok(None) => {}
7972+ _ => panic!(),
7973+ };
7974+ match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
7975+ Ok(None) => {}
7976+ _ => panic!(),
7977+ };
7978+ match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
7979+ Ok(Some(Command::Payload(_))) => {}
7980+ _ => panic!(),
7981+ };
7982+ match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
7983+ Ok(Some(Command::Requests(_))) => {}
7984+ _ => panic!(),
7985+ };
7986+ }
7987+
7988+ #[test]
7989+ fn sequential_commands_pipeline() {
7990+ let mut transport = Transport::default().pipelining(true);
7991+ match transport.decode(&mut BytesMut::from("HELO example.org\r\n")) {
7992+ Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
7993+ _ => panic!(),
7994+ };
7995+ match transport.decode(&mut BytesMut::from("DATA\r\n")) {
7996+ Ok(Some(command)) => assert!(matches!(command, Command::Requests(_))),
7997+ _ => panic!(),
7998+ };
7999+ match transport.decode(&mut BytesMut::from("Subject: Hello World\r\n")) {
8000+ Ok(None) => {}
8001+ _ => panic!(),
8002+ };
8003+ match transport.decode(&mut BytesMut::from("AAAAAAABBBBBBCCCCCCC")) {
8004+ Ok(None) => {}
8005+ _ => panic!(),
8006+ };
8007+ match transport.decode(&mut BytesMut::from("DDDDDDDEEEEEEEFFFFFF")) {
8008+ Ok(None) => {}
8009+ _ => panic!(),
8010+ };
8011+ match transport.decode(&mut BytesMut::from("\r\n.\r\n")) {
8012+ Ok(Some(Command::Payload(_))) => {}
8013+ _ => panic!(),
8014+ };
8015+ match transport.decode(&mut BytesMut::from("QUIT\r\n")) {
8016+ Ok(Some(Command::Requests(_))) => {}
8017+ _ => panic!(),
8018+ };
8019+ }
8020+ }
8021 diff --git a/src/session.rs b/src/session.rs
8022new file mode 100644
8023index 0000000..9deeaa7
8024--- /dev/null
8025+++ b/src/session.rs
8026 @@ -0,0 +1,1213 @@
8027+ use std::fmt::Display;
8028+ use std::net::IpAddr;
8029+ use std::str::FromStr;
8030+ use std::sync::{Arc, Mutex};
8031+
8032+ use email_address::EmailAddress;
8033+
8034+ use mail_parser::{Message, MessageParser};
8035+ use smtp_proto::{EhloResponse, Request, Response as SmtpResponse};
8036+ use url::Host;
8037+
8038+ use crate::plain_auth::{AuthData, AuthError};
8039+ use crate::expand::ExpansionError;
8040+ use crate::smtp_response;
8041+ use crate::verify::VerifyError;
8042+
8043+ /// Default help banner returned from a HELP command without any parameters
8044+ pub const DEFAULT_HELP_BANNER: &str = r#"
8045+ Maitred ESMTP Server:
8046+ see https://ayllu-forge.org/ayllu/maitred for more information.
8047+ "#;
8048+
8049+ /// Maximum message size the server will accept.
8050+ pub const DEFAULT_MAXIMUM_MESSAGE_SIZE: u64 = 5_000_000;
8051+
8052+ /// Default greeting returned by the server upon initial connection.
8053+ pub const DEFAULT_GREETING: &str = "Maitred ESMTP Server";
8054+
8055+ // TODO:
8056+ // 250-8BITMIME
8057+ // 250-DSN
8058+ // 250-SMTPUTF8
8059+
8060+ /// Default SMTP capabilities advertised by the server.
8061+ pub const DEFAULT_CAPABILITIES: u32 = smtp_proto::EXT_SIZE
8062+ | smtp_proto::EXT_ENHANCED_STATUS_CODES
8063+ | smtp_proto::EXT_PIPELINING
8064+ | smtp_proto::EXT_8BIT_MIME;
8065+
8066+ /// Wrapper around an SMTP message to send to the client.
8067+ #[derive(Debug, Clone)]
8068+ pub enum Response<T>
8069+ where
8070+ T: Display,
8071+ {
8072+ General(SmtpResponse<T>),
8073+ Ehlo(EhloResponse<T>),
8074+ }
8075+
8076+ impl Response<String> {
8077+ pub fn is_fatal(&self) -> bool {
8078+ match self {
8079+ Response::General(resp) => resp.code >= 500,
8080+ Response::Ehlo(_) => false,
8081+ }
8082+ }
8083+ }
8084+
8085+ impl<T> PartialEq for Response<T>
8086+ where
8087+ T: Display,
8088+ {
8089+ fn eq(&self, other: &Self) -> bool {
8090+ match self {
8091+ Response::General(req) => match other {
8092+ Response::General(other) => req.to_string() == other.to_string(),
8093+ Response::Ehlo(_) => false,
8094+ },
8095+ Response::Ehlo(req) => match other {
8096+ Response::General(_) => false,
8097+ Response::Ehlo(other) => {
8098+ // FIXME
8099+ req.capabilities == other.capabilities
8100+ && req.hostname.to_string() == other.hostname.to_string()
8101+ && req.deliver_by == other.deliver_by
8102+ && req.size == other.size
8103+ && req.auth_mechanisms == other.auth_mechanisms
8104+ && req.future_release_datetime.eq(&req.future_release_datetime)
8105+ && req.future_release_interval.eq(&req.future_release_interval)
8106+ }
8107+ },
8108+ }
8109+ }
8110+ }
8111+
8112+ impl<T> Eq for Response<T> where T: Display {}
8113+
8114+ #[derive(Clone, Default)]
8115+ pub(crate) struct Capabilities {
8116+ mode: Option<Mode>,
8117+ capabilities: u32,
8118+ }
8119+
8120+ impl Capabilities {
8121+ pub fn enabled(&self, capability: u32) -> bool {
8122+ self.mode
8123+ .as_ref()
8124+ .is_some_and(|mode| matches!(mode, Mode::Extended))
8125+ && self.capabilities & capability != 0
8126+ }
8127+ }
8128+
8129+ /// Callback helpers for various session behavior
8130+ pub mod callback {
8131+ use super::*;
8132+
8133+ /// Set the session message after it has been processed by the called
8134+ pub struct SetMessage {
8135+ pub(crate) inner: Arc<Mutex<Inner>>,
8136+ pub(crate) capabilities: Capabilities,
8137+ }
8138+
8139+ impl SetMessage {
8140+ /// checks if 8BITMIME is supported
8141+ fn check_body(&self, body: &[u8]) -> Result<(), Response<String>> {
8142+ if !self.capabilities.enabled(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() {
8143+ return Err(smtp_response!(
8144+ 500,
8145+ 0,
8146+ 0,
8147+ 0,
8148+ "Non ASCII characters found in message body"
8149+ ));
8150+ }
8151+ Ok(())
8152+ }
8153+ /// Called each time a message is ready for processing, will do spf
8154+ /// validation if it is configured.
8155+ fn accept_payload(&self, inner: &Inner, ip_addr: IpAddr, payload: &[u8]) -> Action {
8156+ if inner.rcpt_to.is_none() {
8157+ return Action::Send(smtp_response!(500, 0, 0, 0, "RCPT TO is missing"));
8158+ }
8159+ if inner.hostname.is_none() {
8160+ return Action::Send(smtp_response!(500, 0, 0, 0, "Hostname is missing"));
8161+ }
8162+ // let copied = payload.to_vec();
8163+ if let Err(response) = self.check_body(payload) {
8164+ return Action::Send(response);
8165+ };
8166+ let parser = MessageParser::new();
8167+ match parser.parse(payload) {
8168+ Some(message) => Action::Envelope {
8169+ initial_response: smtp_response!(250, 0, 0, 0, "OK"),
8170+ envelope: Envelope {
8171+ ip_addr,
8172+ body: message.into_owned(),
8173+ // FIXME
8174+ mail_from: inner.mail_from.clone().unwrap(),
8175+ rcpt_to: inner.rcpt_to.clone().unwrap(),
8176+ hostname: inner.hostname.clone().unwrap(),
8177+ },
8178+ },
8179+ None => Action::Send(smtp_response!(500, 0, 0, 0, "Cannot parse message payload")),
8180+ }
8181+ }
8182+
8183+ pub fn call(&self, ip_addr: IpAddr, message: &[u8]) -> Action {
8184+ let inner = self.inner.lock().unwrap();
8185+ self.accept_payload(&inner, ip_addr, message)
8186+ }
8187+ }
8188+
8189+ /// Plain authentication
8190+ pub struct PlainAuth(pub(crate) Arc<Mutex<Inner>>);
8191+
8192+ impl PlainAuth {
8193+ pub fn call(&self, auth_response: Result<String, AuthError>) -> Action {
8194+ match auth_response {
8195+ Ok(authcid) => {
8196+ tracing::info!("Successfully Authenticated");
8197+ let mut inner = self.0.lock().unwrap();
8198+ inner.authenticated_id = Some(authcid.clone());
8199+ Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
8200+ }
8201+ Err(e) => Action::Send(e.into()),
8202+ }
8203+ }
8204+ }
8205+
8206+ /// SMTP VRFY helper
8207+ pub struct Verify;
8208+
8209+ impl Verify {
8210+ pub fn call(&self, verify_response: Result<(), VerifyError>) -> Action {
8211+ match verify_response {
8212+ Ok(_) => Action::Send(smtp_response!(200, 0, 0, 0, "OK")),
8213+ Err(e) => Action::Send(e.into()),
8214+ }
8215+ }
8216+ }
8217+
8218+ /// SMTP EXPN helper
8219+ pub struct Expand;
8220+
8221+ impl Expand {
8222+ pub fn call(&self, addresses: Result<Vec<EmailAddress>, ExpansionError>) -> Action {
8223+ match addresses {
8224+ Ok(addresses) => {
8225+ let mut responses = vec![smtp_response!(250, 0, 0, 0, "OK")];
8226+ responses.extend(
8227+ addresses
8228+ .iter()
8229+ .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())),
8230+ );
8231+ Action::SendMany(responses)
8232+ }
8233+ Err(e) => Action::Send(e.into()),
8234+ }
8235+ }
8236+ }
8237+ }
8238+
8239+ /// An Envelope containing an e-mail message created from the session.
8240+ #[derive(Clone, Debug)]
8241+ pub struct Envelope {
8242+ pub body: Message<'static>,
8243+ pub mail_from: EmailAddress,
8244+ pub rcpt_to: Vec<EmailAddress>,
8245+ pub ip_addr: IpAddr,
8246+ pub hostname: Host,
8247+ }
8248+
8249+ /// Action for the server implementor to take probably asynchronously.
8250+ pub enum Action {
8251+ Send(Response<String>),
8252+ SendMany(Vec<Response<String>>),
8253+ Message {
8254+ initial_response: Response<String>,
8255+ cb: callback::SetMessage,
8256+ },
8257+ Envelope {
8258+ initial_response: Response<String>,
8259+ envelope: Envelope,
8260+ },
8261+ PlainAuth {
8262+ authcid: String,
8263+ authzid: String,
8264+ password: String,
8265+ cb: callback::PlainAuth,
8266+ },
8267+ Verify {
8268+ address: EmailAddress,
8269+ cb: callback::Verify,
8270+ },
8271+ Expand {
8272+ address: String,
8273+ cb: callback::Expand,
8274+ },
8275+ StartTls(Response<String>),
8276+ Quit(Response<String>),
8277+ }
8278+
8279+ impl Display for Action {
8280+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
8281+ match self {
8282+ Action::Send(response) => match response {
8283+ Response::General(response) => {
8284+ f.write_fmt(format_args!("Send:{}", &response.to_string()))
8285+ }
8286+ Response::Ehlo(ehlo_response) => {
8287+ f.write_fmt(format_args!("Send:{:?}", ehlo_response))
8288+ }
8289+ },
8290+ Action::SendMany(vec) => {
8291+ f.write_str("Send Many:\n")?;
8292+ vec.iter().for_each(|message| match message {
8293+ Response::General(response) => f
8294+ .write_fmt(format_args!("{}\n", &response.to_string()))
8295+ .unwrap(),
8296+ Response::Ehlo(_ehlo_response) => unreachable!(),
8297+ });
8298+ Ok(())
8299+ }
8300+ Action::Message {
8301+ initial_response,
8302+ cb: _,
8303+ } => match initial_response {
8304+ Response::General(response) => f.write_fmt(format_args!("Message:\n{}", response)),
8305+ Response::Ehlo(_ehlo_response) => unreachable!(),
8306+ },
8307+ Action::Envelope {
8308+ initial_response,
8309+ envelope,
8310+ } => f.write_fmt(format_args!(
8311+ "Envelope: {:?}, {}",
8312+ initial_response, envelope.mail_from
8313+ )),
8314+ Action::PlainAuth {
8315+ authcid,
8316+ authzid,
8317+ password: _,
8318+ cb: _,
8319+ } => f.write_fmt(format_args!("Plain Auth: {} {}", authcid, authzid)),
8320+ Action::Verify { address, cb: _ } => f.write_fmt(format_args!("Verify: {}", address)),
8321+ Action::Expand { address, cb: _ } => f.write_fmt(format_args!("Expand: {}", address)),
8322+ Action::StartTls(response) => match response {
8323+ Response::General(response) => f.write_str(&response.to_string()),
8324+ Response::Ehlo(_ehlo_response) => unreachable!(),
8325+ },
8326+ Action::Quit(response) => match response {
8327+ Response::General(response) => {
8328+ f.write_fmt(format_args!("Quit: {}", &response.to_string()))
8329+ }
8330+ Response::Ehlo(_ehlo_response) => unreachable!(),
8331+ },
8332+ }
8333+ }
8334+ }
8335+
8336+ /// If the session was started with HELO or ELHO.
8337+ #[derive(Clone)]
8338+ enum Mode {
8339+ Legacy,
8340+ Extended,
8341+ }
8342+
8343+ /// Sent when the connection exceeds the maximum configured timeout
8344+ pub fn timeout(message: &str) -> Response<String> {
8345+ smtp_response!(421, 4, 4, 2, format!("Timeout exceeded: {}", message))
8346+ }
8347+
8348+ pub fn tls_already_active() -> Response<String> {
8349+ smtp_response!(400, 0, 0, 0, "TLS is already active")
8350+ }
8351+
8352+ pub fn spf_rejection() -> Response<String> {
8353+ smtp_response!(500, 0, 0, 0, "SPF Verification Failed")
8354+ }
8355+
8356+ pub fn smtp_error_to_response(e: smtp_proto::Error) -> Response<String> {
8357+ match e {
8358+ smtp_proto::Error::NeedsMoreData { bytes_left: _ } => {
8359+ // TODO
8360+ smtp_response!(500, 0, 0, 0, e.to_string())
8361+ }
8362+ smtp_proto::Error::UnknownCommand => {
8363+ smtp_response!(500, 5, 5, 1, "Invalid Command")
8364+ }
8365+ smtp_proto::Error::InvalidSenderAddress => {
8366+ smtp_response!(501, 5, 1, 8, e.to_string())
8367+ }
8368+ smtp_proto::Error::InvalidRecipientAddress => {
8369+ smtp_response!(501, 5, 1, 3, e.to_string())
8370+ }
8371+ smtp_proto::Error::SyntaxError { syntax: _ } => {
8372+ smtp_response!(501, 5, 5, 2, e.to_string())
8373+ }
8374+ smtp_proto::Error::InvalidParameter { param: _ } => {
8375+ // TODO
8376+ smtp_response!(500, 0, 0, 0, e.to_string())
8377+ }
8378+ smtp_proto::Error::UnsupportedParameter { param: _ } => {
8379+ // TODO
8380+ smtp_response!(500, 0, 0, 0, e.to_string())
8381+ }
8382+ smtp_proto::Error::ResponseTooLong => {
8383+ // TODO
8384+ smtp_response!(500, 0, 0, 0, e.to_string())
8385+ }
8386+ smtp_proto::Error::InvalidResponse { code: _ } => {
8387+ // TODO
8388+ smtp_response!(500, 0, 0, 0, e.to_string())
8389+ }
8390+ }
8391+ }
8392+
8393+ /// Extract a host from HELO/EHLO per RFC5321 4.1.3
8394+ fn parse_host(host: &str) -> String {
8395+ // confusingly the url library determines if an address is IPv6 by checking
8396+ // for [ ] but SMTP uses "tags" to determine this.
8397+ let n_periods = host
8398+ .chars()
8399+ .fold(0, |accm, c| if c == '.' { accm + 1 } else { accm });
8400+ if n_periods == 3 {
8401+ host.trim_start_matches("[")
8402+ .trim_end_matches("]")
8403+ .to_string()
8404+ } else if host.contains("IPv6:") {
8405+ format!("[{}]", host.replace("IPv6:", "").trim())
8406+ } else {
8407+ host.to_string()
8408+ }
8409+ }
8410+
8411+ /// session runtime flags
8412+ #[derive(Clone, Default)]
8413+ struct Flags {
8414+ authentication: bool,
8415+ starttls: bool,
8416+ vrfy: bool,
8417+ expn: bool,
8418+ }
8419+
8420+ #[derive(Clone, Default)]
8421+ pub(crate) struct Inner {
8422+ /// mailto address
8423+ mail_from: Option<EmailAddress>,
8424+ /// rcpt address
8425+ rcpt_to: Option<Vec<EmailAddress>>,
8426+ /// hostname per HELO
8427+ hostname: Option<Host>,
8428+
8429+ initialized: Option<Mode>,
8430+ // previously ran commands
8431+ // TODO pipeline still partially broken
8432+ history: Vec<Request<String>>,
8433+ spf_verified_host: Option<String>,
8434+ authenticated_id: Option<String>,
8435+ }
8436+
8437+ impl Inner {
8438+ /// Reset the connection to it's default state but after a HELO/ELHO has
8439+ /// been issued successfully.
8440+ pub fn reset(&mut self) {
8441+ self.mail_from = None;
8442+ self.rcpt_to = None;
8443+ // self.hostname = None;
8444+ self.initialized = None;
8445+ self.history = Vec::new();
8446+ self.spf_verified_host = None;
8447+ }
8448+ }
8449+
8450+ /// State machine that corresponds to a single SMTP session, calls to next
8451+ /// return actions that the caller is expected to implement in a transport.
8452+ /// ```rust,no_run
8453+ #[doc = include_str!("../examples/session.rs")]
8454+ ///
8455+ #[derive(Clone)]
8456+ pub struct Session {
8457+ // /// mailto address
8458+ // mail_from: Option<EmailAddress>,
8459+ // /// rcpt address
8460+ // rcpt_to: Option<Vec<EmailAddress>>,
8461+ // /// hostname per HELO
8462+ // hostname: Option<Host>,
8463+
8464+ // initialized: Option<Mode>,
8465+ // // previously ran commands
8466+ // // TODO pipeline still partially broken
8467+ // history: Vec<Request<String>>,
8468+ inner: Arc<Mutex<Inner>>,
8469+
8470+ // session opts
8471+ our_hostname: Option<String>, // required
8472+ maximum_size: u64,
8473+ capabilities: u32,
8474+ help_banner: String,
8475+ greeting: String,
8476+ tls_active: bool,
8477+
8478+ flags: Flags,
8479+ }
8480+
8481+ impl Default for Session {
8482+ fn default() -> Self {
8483+ Session {
8484+ inner: Arc::new(Mutex::new(Inner::default())),
8485+ our_hostname: None,
8486+ maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE,
8487+ capabilities: DEFAULT_CAPABILITIES,
8488+ help_banner: DEFAULT_HELP_BANNER.to_string(),
8489+ greeting: DEFAULT_GREETING.to_string(),
8490+ tls_active: false,
8491+ flags: Flags::default(),
8492+ }
8493+ }
8494+ }
8495+
8496+ impl Session {
8497+ pub fn our_hostname(mut self, hostname: &str) -> Self {
8498+ self.our_hostname = Some(hostname.to_string());
8499+ self
8500+ }
8501+
8502+ pub fn maximum_size(mut self, maximum_size: u64) -> Self {
8503+ self.maximum_size = maximum_size;
8504+ self
8505+ }
8506+
8507+ pub fn capabilities(mut self, capabilities: u32) -> Self {
8508+ self.capabilities = capabilities;
8509+ self
8510+ }
8511+
8512+ pub fn help_banner(mut self, help_banner: &str) -> Self {
8513+ self.help_banner = help_banner.to_string();
8514+ self
8515+ }
8516+
8517+ pub fn greeting_banner(mut self, greeting: &str) -> Self {
8518+ self.greeting = greeting.to_string();
8519+ self
8520+ }
8521+
8522+ pub fn authentication(mut self, enabled: bool) -> Self {
8523+ self.flags.authentication = enabled;
8524+ self.capabilities |= smtp_proto::EXT_AUTH;
8525+ self
8526+ }
8527+
8528+ pub fn starttls(mut self, enabled: bool) -> Self {
8529+ self.flags.starttls = enabled;
8530+ self.capabilities |= smtp_proto::EXT_START_TLS;
8531+ self
8532+ }
8533+
8534+ pub fn vrfy_enabled(mut self, enabled: bool) -> Self {
8535+ self.flags.vrfy = enabled;
8536+ self
8537+ }
8538+
8539+ pub fn expn_enabled(mut self, enabled: bool) -> Self {
8540+ self.flags.expn = enabled;
8541+ self
8542+ }
8543+
8544+ pub fn tls_active(mut self, active: bool) -> Self {
8545+ self.tls_active = active;
8546+ self
8547+ }
8548+
8549+ /// A greeting must be sent at the start of an SMTP connection when it is
8550+ /// first initialized.
8551+ /// FIXME
8552+ pub fn greeting(&self) -> Response<String> {
8553+ smtp_response!(
8554+ 220,
8555+ 2,
8556+ 0,
8557+ 0,
8558+ format!(
8559+ "{} {}",
8560+ self.our_hostname.clone().expect("hostname not configured"),
8561+ self.greeting
8562+ )
8563+ )
8564+ }
8565+
8566+ // /// Ensure that the session has been initialized otherwise return an error
8567+ fn check_initialized(&self, inner: &Inner) -> Result<(), Response<String>> {
8568+ if inner.initialized.is_none() {
8569+ return Err(smtp_response!(
8570+ 500,
8571+ 5,
8572+ 5,
8573+ 1,
8574+ "It's polite to say EHLO first"
8575+ ));
8576+ }
8577+ Ok(())
8578+ }
8579+
8580+ /// Process the SMTP command returning the action sometimes with a callback
8581+ /// that the implementor needs to take.
8582+ pub fn next(&mut self, req: Option<&Request<String>>) -> Action {
8583+ let mut inner = self.inner.lock().unwrap();
8584+ if let Some(req) = req {
8585+ inner.history.push(req.clone());
8586+ }
8587+ match req {
8588+ None => {
8589+ tracing::info!("Sending initial greeting");
8590+ Action::Send(smtp_response!(
8591+ 220,
8592+ 2,
8593+ 0,
8594+ 0,
8595+ format!(
8596+ "{} {}",
8597+ self.our_hostname.clone().unwrap_or_default(),
8598+ self.greeting
8599+ )
8600+ ))
8601+ }
8602+ Some(Request::Ehlo { host }) => {
8603+ match Host::parse(&parse_host(host)) {
8604+ Ok(hostname) => {
8605+ inner.hostname = Some(hostname);
8606+ }
8607+ Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
8608+ };
8609+ inner.reset();
8610+ inner.initialized = Some(Mode::Extended);
8611+ let mut resp = EhloResponse::new(format!("Hello {}", host));
8612+ resp.capabilities = self.capabilities;
8613+ resp.size = self.maximum_size as usize;
8614+ if self.flags.authentication {
8615+ resp.auth_mechanisms = smtp_proto::AUTH_PLAIN;
8616+ }
8617+ Action::Send(Response::Ehlo(resp))
8618+ }
8619+ Some(Request::Lhlo { host }) => {
8620+ match Host::parse(&parse_host(host)) {
8621+ Ok(hostname) => {
8622+ inner.hostname = Some(hostname);
8623+ }
8624+ Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
8625+ };
8626+ inner.reset();
8627+ inner.initialized = Some(Mode::Legacy);
8628+ Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
8629+ }
8630+ Some(Request::Helo { host }) => {
8631+ match Host::parse(&parse_host(host)) {
8632+ Ok(hostname) => {
8633+ inner.hostname = Some(hostname);
8634+ }
8635+ Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
8636+ };
8637+ inner.reset();
8638+ inner.initialized = Some(Mode::Legacy);
8639+ Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
8640+ }
8641+ Some(Request::Mail { from }) => {
8642+ if let Some(err) = self.check_initialized(&inner).err() {
8643+ return Action::Send(err);
8644+ }
8645+ let mail_from = match EmailAddress::from_str(&from.address) {
8646+ Ok(addr) => addr,
8647+ Err(e) => {
8648+ return Action::Send(smtp_response!(
8649+ 500,
8650+ 0,
8651+ 0,
8652+ 0,
8653+ format!("cannot parse: {} {}", from.address, e)
8654+ ))
8655+ }
8656+ };
8657+ inner.mail_from = Some(mail_from.clone());
8658+ Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
8659+ }
8660+ Some(Request::Rcpt { to }) => {
8661+ if let Some(err) = self.check_initialized(&inner).err() {
8662+ return Action::Send(err);
8663+ }
8664+ let rcpt_to = match EmailAddress::from_str(to.address.as_str()) {
8665+ Ok(rcpt_to) => rcpt_to,
8666+ Err(e) => {
8667+ return Action::Send(smtp_response!(
8668+ 500,
8669+ 0,
8670+ 0,
8671+ 0,
8672+ format!("cannot parse: {} {}", to.address, e)
8673+ ))
8674+ }
8675+ };
8676+ if let Some(ref mut rcpts) = inner.rcpt_to {
8677+ rcpts.push(rcpt_to.clone());
8678+ } else {
8679+ inner.rcpt_to = Some(vec![rcpt_to.clone()]);
8680+ }
8681+ Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
8682+ }
8683+ Some(Request::Bdat {
8684+ chunk_size: _,
8685+ is_last: _,
8686+ }) => {
8687+ if let Some(err) = self.check_initialized(&inner).err() {
8688+ return Action::Send(err);
8689+ }
8690+ tracing::info!("Starting binary data transfer");
8691+ Action::Message {
8692+ initial_response: smtp_response!(
8693+ 354,
8694+ 0,
8695+ 0,
8696+ 0,
8697+ "Starting BDAT data transfer".to_string()
8698+ ),
8699+ cb: callback::SetMessage {
8700+ inner: self.inner.clone(),
8701+ capabilities: Capabilities {
8702+ mode: inner.initialized.clone(),
8703+ capabilities: self.capabilities,
8704+ },
8705+ },
8706+ }
8707+ }
8708+ // After an AUTH command has been successfully completed, no more
8709+ // AUTH commands may be issued in the same session. After a
8710+ // successful AUTH command completes, a server MUST reject any
8711+ // further AUTH commands with a 503 reply.
8712+ Some(Request::Auth {
8713+ mechanism,
8714+ initial_response,
8715+ }) => {
8716+ if let Some(err) = self.check_initialized(&inner).err() {
8717+ return Action::Send(err);
8718+ }
8719+ if self.flags.authentication {
8720+ if *mechanism != smtp_proto::AUTH_PLAIN {
8721+ // only plain auth is supported
8722+ return Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported"));
8723+ }
8724+ let auth_data = match AuthData::try_from(initial_response.as_str()) {
8725+ Ok(auth_data) => auth_data,
8726+ Err(e) => return Action::Send(e.into()),
8727+ };
8728+ Action::PlainAuth {
8729+ authcid: auth_data.authcid(),
8730+ authzid: auth_data.authzid(),
8731+ password: auth_data.passwd(),
8732+ cb: callback::PlainAuth(self.inner.clone()),
8733+ }
8734+ } else {
8735+ Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported"))
8736+ }
8737+ }
8738+ Some(Request::Noop { value: _ }) => {
8739+ if let Some(err) = self.check_initialized(&inner).err() {
8740+ return Action::Send(err);
8741+ }
8742+ Action::Send(smtp_response!(250, 0, 0, 0, "OK".to_string()))
8743+ }
8744+ Some(Request::Vrfy { value }) => {
8745+ if let Some(err) = self.check_initialized(&inner).err() {
8746+ return Action::Send(err);
8747+ }
8748+ if self.flags.vrfy {
8749+ let address = match EmailAddress::from_str(value) {
8750+ Ok(addr) => addr,
8751+ Err(e) => {
8752+ return Action::Send(smtp_response!(
8753+ 500,
8754+ 0,
8755+ 0,
8756+ 0,
8757+ format!("cannot parse: {} {}", value, e)
8758+ ))
8759+ }
8760+ };
8761+ Action::Verify {
8762+ address,
8763+ cb: callback::Verify,
8764+ }
8765+ } else {
8766+ Action::Send(smtp_response!(500, 0, 0, 0, "VRFY Unavailable"))
8767+ }
8768+ }
8769+ Some(Request::Expn { value }) => {
8770+ if let Some(err) = self.check_initialized(&inner).err() {
8771+ return Action::Send(err);
8772+ }
8773+ if self.flags.expn && inner.authenticated_id.is_some() {
8774+ Action::Expand {
8775+ address: value.clone(),
8776+ cb: callback::Expand,
8777+ }
8778+ } else {
8779+ Action::Send(smtp_response!(500, 0, 0, 0, "EXPN Unavailable"))
8780+ }
8781+ }
8782+ Some(Request::Help { value }) => {
8783+ if let Some(err) = self.check_initialized(&inner).err() {
8784+ return Action::Send(err);
8785+ }
8786+ if value.is_empty() {
8787+ Action::Send(smtp_response!(250, 0, 0, 0, self.help_banner))
8788+ } else {
8789+ Action::Send(smtp_response!(
8790+ 500,
8791+ 0,
8792+ 0,
8793+ 0,
8794+ format!("Help for {} is not currently available", value)
8795+ ))
8796+ }
8797+ }
8798+ Some(Request::Etrn { name: _ }) => {
8799+ Action::Send(smtp_response!(500, 0, 0, 0, "ETRN is not supported"))
8800+ }
8801+ Some(Request::Atrn { domains: _ }) => {
8802+ Action::Send(smtp_response!(500, 0, 0, 0, "ATRN is not supported"))
8803+ }
8804+ Some(Request::Burl { uri: _, is_last: _ }) => {
8805+ Action::Send(smtp_response!(500, 0, 0, 0, "BURL is not supported"))
8806+ }
8807+ Some(Request::StartTls) => {
8808+ if self.flags.starttls && !self.tls_active {
8809+ Action::StartTls(smtp_response!(220, 0, 0, 0, "Go ahead"))
8810+ } else if self.flags.starttls && self.tls_active {
8811+ Action::Send(tls_already_active())
8812+ } else {
8813+ Action::Send(smtp_response!(
8814+ 500,
8815+ 0,
8816+ 0,
8817+ 0,
8818+ format!("STARTTLS is not supported")
8819+ ))
8820+ }
8821+ }
8822+ Some(Request::Data) => {
8823+ if let Some(err) = self.check_initialized(&inner).err() {
8824+ return Action::Send(err);
8825+ }
8826+ tracing::info!("Starting data transfer");
8827+ Action::Message {
8828+ initial_response: smtp_response!(
8829+ 354,
8830+ 0,
8831+ 0,
8832+ 0,
8833+ "Reading data input, end the message with <CRLF>.<CRLF>".to_string()
8834+ ),
8835+ cb: callback::SetMessage {
8836+ inner: self.inner.clone(),
8837+ capabilities: Capabilities {
8838+ mode: inner.initialized.clone(),
8839+ capabilities: self.capabilities,
8840+ },
8841+ },
8842+ }
8843+ }
8844+ Some(Request::Rset) => {
8845+ if let Some(err) = self.check_initialized(&inner).err() {
8846+ return Action::Send(err);
8847+ }
8848+ inner.reset();
8849+ Action::Send(smtp_response!(200, 0, 0, 0, "".to_string()))
8850+ }
8851+ Some(Request::Quit) => Action::Quit(smtp_response!(221, 0, 0, 0, "Ciao!".to_string())),
8852+ }
8853+ }
8854+ }
8855+
8856+ #[cfg(test)]
8857+ mod test {
8858+
8859+ use base64::engine::general_purpose::STANDARD;
8860+ use base64::prelude::*;
8861+ use smtp_proto::MailFrom;
8862+
8863+ use super::*;
8864+
8865+ const EXAMPLE_HOSTNAME: &str = "example.org";
8866+
8867+ fn equal(actual: &Action, expected: &Action) -> bool {
8868+ let is_equal = match actual {
8869+ Action::Send(response) => {
8870+ matches!(expected, Action::Send(other) if response.eq(other))
8871+ }
8872+ Action::SendMany(actual) => match expected {
8873+ Action::SendMany(expected) => actual.iter().enumerate().all(|(i, resp)| {
8874+ if let Some(expected_resp) = expected.get(i) {
8875+ resp.eq(expected_resp)
8876+ } else {
8877+ false
8878+ }
8879+ }),
8880+ _ => false,
8881+ },
8882+ Action::Message {
8883+ initial_response: _,
8884+ cb: _,
8885+ } => todo!(),
8886+ Action::Envelope {
8887+ initial_response: _,
8888+ envelope: _,
8889+ } => {
8890+ matches!(
8891+ expected,
8892+ Action::Envelope {
8893+ initial_response: _,
8894+ envelope: _
8895+ }
8896+ )
8897+ }
8898+ Action::PlainAuth {
8899+ authcid: _,
8900+ authzid: _,
8901+ password: _,
8902+ cb: _,
8903+ } => todo!(),
8904+ Action::Verify { address: _, cb: _ } => todo!(),
8905+ Action::Expand { address: _, cb: _ } => todo!(),
8906+ Action::StartTls(_response) => todo!(),
8907+ Action::Quit(response) => {
8908+ matches!(expected, Action::Quit(other) if response.eq(other))
8909+ }
8910+ };
8911+
8912+ if !is_equal {
8913+ println!("Responses Differ:");
8914+ println!("Expected:");
8915+ println!("{}", expected);
8916+ println!("Actual:");
8917+ println!("{}", actual);
8918+ return false;
8919+ };
8920+
8921+ true
8922+ }
8923+
8924+ #[test]
8925+ fn session_greeting() {
8926+ let mut session = Session::default();
8927+ assert!(matches!(session.next(None), Action::Send(_)))
8928+ }
8929+
8930+ #[test]
8931+ fn session_hello_quit() {
8932+ let mut session = Session::default();
8933+ assert!(equal(
8934+ &session.next(Some(&Request::Helo {
8935+ host: EXAMPLE_HOSTNAME.to_string(),
8936+ })),
8937+ &Action::Send(smtp_response!(
8938+ 250,
8939+ 0,
8940+ 0,
8941+ 0,
8942+ String::from("Hello example.org")
8943+ )),
8944+ ));
8945+ assert!(equal(
8946+ &session.next(Some(&Request::Quit {})),
8947+ &Action::Quit(smtp_response!(221, 0, 0, 0, String::from("Ciao!"))),
8948+ ));
8949+
8950+ assert!(session
8951+ .inner
8952+ .lock()
8953+ .unwrap()
8954+ .hostname
8955+ .as_ref()
8956+ .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
8957+ }
8958+
8959+ #[test]
8960+ fn session_command_with_no_helo() {
8961+ let mut session = Session::default();
8962+ assert!(equal(
8963+ &session.next(Some(&Request::Mail {
8964+ from: MailFrom {
8965+ address: String::from("fuu@example.org"),
8966+ ..Default::default()
8967+ }
8968+ })),
8969+ &Action::Send(smtp_response!(
8970+ 500,
8971+ 5,
8972+ 5,
8973+ 1,
8974+ String::from("It's polite to say EHLO first")
8975+ ))
8976+ ))
8977+ }
8978+
8979+ #[test]
8980+ fn session_authenticate() {
8981+ let session = &mut Session::default().authentication(true);
8982+ assert!(equal(
8983+ &session.next(Some(&Request::Helo {
8984+ host: EXAMPLE_HOSTNAME.to_string(),
8985+ })),
8986+ &Action::Send(smtp_response!(
8987+ 250,
8988+ 0,
8989+ 0,
8990+ 0,
8991+ String::from("Hello example.org")
8992+ )),
8993+ ));
8994+
8995+ {
8996+ let auth = session.next(Some(&Request::Auth {
8997+ mechanism: smtp_proto::AUTH_PLAIN,
8998+ initial_response: STANDARD.encode(b"\0hello\0world"),
8999+ }));
9000+ match auth {
9001+ Action::PlainAuth {
9002+ authcid,
9003+ authzid,
9004+ password,
9005+ cb,
9006+ } => {
9007+ assert!(authcid == "hello");
9008+ assert!(authzid == "hello");
9009+ assert!(password == "world");
9010+ assert!(equal(
9011+ &cb.call(Ok(authcid.clone())),
9012+ &Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
9013+ ));
9014+ }
9015+ _ => panic!("Unexpected response"),
9016+ };
9017+ };
9018+
9019+ assert!(session
9020+ .inner
9021+ .lock()
9022+ .unwrap()
9023+ .authenticated_id
9024+ .as_ref()
9025+ .is_some_and(|id| id == "hello"));
9026+ }
9027+
9028+ #[test]
9029+ fn session_expand() {
9030+ let session = &mut Session::default().authentication(true).expn_enabled(true);
9031+ session.inner = Arc::new(Mutex::new(Inner {
9032+ initialized: Some(Mode::Extended),
9033+ authenticated_id: Some("hello".to_string()),
9034+ ..Default::default()
9035+ }));
9036+ match session.next(Some(&Request::Expn {
9037+ value: String::from("group@baz.com"),
9038+ })) {
9039+ Action::Expand { address: _, cb } => {
9040+ assert!(equal(
9041+ &cb.call(Ok(vec![
9042+ EmailAddress::new_unchecked("fuu@bar.com"),
9043+ EmailAddress::new_unchecked("baz@qux.com")
9044+ ])),
9045+ &Action::SendMany(vec![
9046+ smtp_response!(250, 0, 0, 0, "OK"),
9047+ smtp_response!(250, 0, 0, 0, "fuu@bar.com"),
9048+ smtp_response!(250, 0, 0, 0, "baz@qux.com")
9049+ ])
9050+ ));
9051+ }
9052+ _ => panic!("Unexpected response"),
9053+ };
9054+ }
9055+
9056+ #[test]
9057+ fn session_verify() {
9058+ let session = &mut Session::default().authentication(true).vrfy_enabled(true);
9059+ session.inner = Arc::new(Mutex::new(Inner {
9060+ initialized: Some(Mode::Extended),
9061+ authenticated_id: Some("hello".to_string()),
9062+ ..Default::default()
9063+ }));
9064+ match session.next(Some(&Request::Vrfy {
9065+ value: String::from("qux@baz.com"),
9066+ })) {
9067+ Action::Verify { address, cb } => {
9068+ assert!(address.to_string() == "qux@baz.com");
9069+ assert!(equal(
9070+ &cb.call(Ok(())),
9071+ &Action::Send(smtp_response!(200, 0, 0, 0, "OK"))
9072+ ));
9073+ }
9074+ _ => panic!("Unexpected response"),
9075+ };
9076+ }
9077+
9078+ #[test]
9079+ fn session_non_ascii_characters_legacy_smtp() {
9080+ let session = &mut Session::default();
9081+ // non-extended sessions cannot accept non-ascii characters
9082+ session.inner = Arc::new(Mutex::new(Inner {
9083+ initialized: Some(Mode::Legacy),
9084+ hostname: Some(Host::parse("example.org").unwrap()),
9085+ authenticated_id: Some("hello".to_string()),
9086+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
9087+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
9088+ ..Default::default()
9089+ }));
9090+ match session.next(Some(&Request::Data {})) {
9091+ Action::Message {
9092+ initial_response,
9093+ cb,
9094+ } => {
9095+ assert!(equal(
9096+ &Action::Send(initial_response),
9097+ &Action::Send(smtp_response!(
9098+ 354,
9099+ 0,
9100+ 0,
9101+ 0,
9102+ "Reading data input, end the message with <CRLF>.<CRLF>"
9103+ ))
9104+ ));
9105+ let action = cb.call(
9106+ IpAddr::from_str("127.0.0.1").unwrap(),
9107+ r#"
9108+ Subject: Hello World
9109+ 😍😍😍
9110+ "#
9111+ .as_bytes(),
9112+ );
9113+ assert!(equal(
9114+ &action,
9115+ &Action::Send(smtp_response!(
9116+ 500,
9117+ 0,
9118+ 0,
9119+ 0,
9120+ "Non ASCII characters found in message body"
9121+ ))
9122+ ))
9123+ }
9124+ _ => panic!("Unexpected response"),
9125+ };
9126+ }
9127+
9128+ #[test]
9129+ fn session_non_ascii_characters_extended_smtp() {
9130+ let session = &mut Session::default();
9131+ // non-extended sessions cannot accept non-ascii characters
9132+ session.inner = Arc::new(Mutex::new(Inner {
9133+ initialized: Some(Mode::Extended),
9134+ hostname: Some(Host::parse("example.org").unwrap()),
9135+ authenticated_id: Some("hello".to_string()),
9136+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
9137+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
9138+ ..Default::default()
9139+ }));
9140+ match session.next(Some(&Request::Data {})) {
9141+ Action::Message {
9142+ initial_response,
9143+ cb,
9144+ } => {
9145+ assert!(equal(
9146+ &Action::Send(initial_response),
9147+ &Action::Send(smtp_response!(
9148+ 354,
9149+ 0,
9150+ 0,
9151+ 0,
9152+ "Reading data input, end the message with <CRLF>.<CRLF>"
9153+ ))
9154+ ));
9155+ let action = cb.call(
9156+ IpAddr::from_str("127.0.0.1").unwrap(),
9157+ r#"
9158+ Subject: Hello World
9159+ 😍😍😍
9160+ "#
9161+ .as_bytes(),
9162+ );
9163+ assert!(equal(
9164+ &action,
9165+ &Action::Envelope {
9166+ initial_response: smtp_response!(250, 0, 0, 0, "OK"),
9167+ envelope: Envelope {
9168+ ip_addr: IpAddr::from_str("127.0.0.1").unwrap(),
9169+ body: Message::default(),
9170+ mail_from: EmailAddress::new_unchecked("fuu@bar.com"),
9171+ rcpt_to: vec![],
9172+ hostname: Host::Domain(String::from("bar.com"))
9173+ }
9174+ }
9175+ ))
9176+ }
9177+ _ => panic!("Unexpected response"),
9178+ };
9179+ }
9180+
9181+ #[test]
9182+ fn session_message_body_ok() {
9183+ let session = &mut Session::default();
9184+ // non-extended sessions cannot accept non-ascii characters
9185+ session.inner = Arc::new(Mutex::new(Inner {
9186+ hostname: Some(Host::parse("example.org").unwrap()),
9187+ initialized: Some(Mode::Extended),
9188+ authenticated_id: Some("hello".to_string()),
9189+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
9190+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
9191+ ..Default::default()
9192+ }));
9193+ {
9194+ match session.next(Some(&Request::Data {})) {
9195+ Action::Message {
9196+ initial_response,
9197+ cb,
9198+ } => {
9199+ assert!(equal(
9200+ &Action::Send(initial_response),
9201+ &Action::Send(smtp_response!(
9202+ 354,
9203+ 0,
9204+ 0,
9205+ 0,
9206+ "Reading data input, end the message with <CRLF>.<CRLF>"
9207+ ))
9208+ ));
9209+ let action = cb.call(
9210+ IpAddr::from_str("127.0.0.1").unwrap(),
9211+ r#"To: <baz@qux.com>
9212+ Subject: Hello World
9213+
9214+ This is an e-mail from a test case!
9215+
9216+ Note that it doesn't end with a "." since that parsing happens as part of the
9217+ transport rather than the session. 🩷
9218+ "#
9219+ .as_bytes(),
9220+ );
9221+ assert!(equal(
9222+ &action,
9223+ &Action::Envelope {
9224+ initial_response: smtp_response!(250, 0, 0, 0, "OK"),
9225+ envelope: Envelope {
9226+ ip_addr: IpAddr::from_str("127.0.0.1").unwrap(),
9227+ body: Message::default(),
9228+ mail_from: EmailAddress::new_unchecked("fuu@bar.com"),
9229+ rcpt_to: vec![],
9230+ hostname: Host::Domain("example.org".to_string())
9231+ }
9232+ }
9233+ ));
9234+ }
9235+ _ => panic!("Unexpected response"),
9236+ };
9237+ };
9238+ }
9239+ }
9240 diff --git a/src/verify.rs b/src/verify.rs
9241new file mode 100644
9242index 0000000..7ade63b
9243--- /dev/null
9244+++ b/src/verify.rs
9245 @@ -0,0 +1,78 @@
9246+ use std::future::Future;
9247+
9248+ use async_trait::async_trait;
9249+ use email_address::EmailAddress;
9250+ use smtp_proto::Response as SmtpResponse;
9251+
9252+ use crate::session::Response;
9253+ use crate::smtp_response;
9254+
9255+ /// An error encountered while verifying an e-mail address
9256+ #[derive(Debug, thiserror::Error)]
9257+ pub enum VerifyError {
9258+ /// Indicates an unspecified error that occurred during expansion
9259+ #[error("Internal Server Error: {0}")]
9260+ Server(String),
9261+ /// Indicates that no group exists with the specified name
9262+ #[error("Group Not Found: {0}")]
9263+ NotFound(String),
9264+ /// Indicates that the input as ambigious and multiple addresses are
9265+ /// associated with the string.
9266+ #[error("Name is Ambiguous: {email}")]
9267+ Ambiguous {
9268+ email: EmailAddress,
9269+ alternatives: Vec<EmailAddress>,
9270+ },
9271+ }
9272+
9273+ #[allow(clippy::from_over_into)]
9274+ impl Into<Response<String>> for VerifyError {
9275+ fn into(self) -> Response<String> {
9276+ match self {
9277+ VerifyError::Server(_) => smtp_response!(500, 0, 0, 0, self.to_string()),
9278+ VerifyError::NotFound(_) => smtp_response!(404, 0, 0, 0, self.to_string()),
9279+ VerifyError::Ambiguous {
9280+ email: _,
9281+ alternatives: _,
9282+ } => smtp_response!(500, 0, 0, 0, self.to_string()),
9283+ }
9284+ }
9285+ }
9286+
9287+ /// Verify that the given e-mail address exists on the server. Servers may
9288+ /// choose to implement nothing or not use this option at all if desired.
9289+ #[async_trait]
9290+ pub trait Verify: Sync + Send {
9291+ /// Verify the e-mail address on the server
9292+ async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError>;
9293+ }
9294+
9295+ /// VerifyFunc wraps an async closure implementing the Verify trait.
9296+ /// # Example
9297+ /// ```rust
9298+ /// use maitred::verify::VerifyFunc;
9299+ /// use maitred::email_address::EmailAddress;
9300+ ///
9301+ /// let verify = VerifyFunc(|address: &EmailAddress| {
9302+ /// async move {
9303+ /// Ok(())
9304+ /// }
9305+ /// });
9306+ ///
9307+ /// ```
9308+ pub struct VerifyFunc<F, T>(pub F)
9309+ where
9310+ F: Fn(&EmailAddress) -> T + Sync + Send,
9311+ T: Future<Output = Result<(), VerifyError>> + Send;
9312+
9313+ #[async_trait]
9314+ impl<F, T> Verify for VerifyFunc<F, T>
9315+ where
9316+ F: Fn(&EmailAddress) -> T + Sync + Send,
9317+ T: Future<Output = Result<(), VerifyError>> + Send,
9318+ {
9319+ async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError> {
9320+ let f = (self.0)(address);
9321+ f.await
9322+ }
9323+ }