Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 17eff5704811a5331c02b3204fa00561e0159489
Timestamp: Thu, 02 Jan 2025 23:21:48 +0000 (2 weeks ago)

+1084 -196 +/-9 browse
ensure session is thread safe, hack together partial client
1diff --git a/Cargo.lock b/Cargo.lock
2index 14b97d8..d58ab30 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -57,6 +57,12 @@ dependencies = [
6 ]
7
8 [[package]]
9+ name = "allocator-api2"
10+ version = "0.2.21"
11+ source = "registry+https://github.com/rust-lang/crates.io-index"
12+ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
13+
14+ [[package]]
15 name = "anstream"
16 version = "0.6.15"
17 source = "registry+https://github.com/rust-lang/crates.io-index"
18 @@ -186,6 +192,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
19 checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
20
21 [[package]]
22+ name = "base64ct"
23+ version = "1.6.0"
24+ source = "registry+https://github.com/rust-lang/crates.io-index"
25+ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
26+
27+ [[package]]
28 name = "bindgen"
29 version = "0.69.4"
30 source = "registry+https://github.com/rust-lang/crates.io-index"
31 @@ -264,12 +276,13 @@ dependencies = [
32
33 [[package]]
34 name = "cc"
35- version = "1.1.6"
36+ version = "1.2.5"
37 source = "registry+https://github.com/rust-lang/crates.io-index"
38- checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f"
39+ checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
40 dependencies = [
41 "jobserver",
42 "libc",
43+ "shlex",
44 ]
45
46 [[package]]
47 @@ -298,6 +311,16 @@ dependencies = [
48 ]
49
50 [[package]]
51+ name = "chumsky"
52+ version = "0.9.3"
53+ source = "registry+https://github.com/rust-lang/crates.io-index"
54+ checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
55+ dependencies = [
56+ "hashbrown",
57+ "stacker",
58+ ]
59+
60+ [[package]]
61 name = "cipher"
62 version = "0.4.4"
63 source = "registry+https://github.com/rust-lang/crates.io-index"
64 @@ -374,6 +397,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
65 checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
66
67 [[package]]
68+ name = "const-oid"
69+ version = "0.9.6"
70+ source = "registry+https://github.com/rust-lang/crates.io-index"
71+ checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
72+
73+ [[package]]
74 name = "constant_time_eq"
75 version = "0.3.1"
76 source = "registry+https://github.com/rust-lang/crates.io-index"
77 @@ -448,6 +477,33 @@ dependencies = [
78 ]
79
80 [[package]]
81+ name = "curve25519-dalek"
82+ version = "4.1.3"
83+ source = "registry+https://github.com/rust-lang/crates.io-index"
84+ checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
85+ dependencies = [
86+ "cfg-if",
87+ "cpufeatures",
88+ "curve25519-dalek-derive",
89+ "digest",
90+ "fiat-crypto",
91+ "rustc_version",
92+ "subtle",
93+ "zeroize",
94+ ]
95+
96+ [[package]]
97+ name = "curve25519-dalek-derive"
98+ version = "0.1.1"
99+ source = "registry+https://github.com/rust-lang/crates.io-index"
100+ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
101+ dependencies = [
102+ "proc-macro2",
103+ "quote",
104+ "syn",
105+ ]
106+
107+ [[package]]
108 name = "data-encoding"
109 version = "2.6.0"
110 source = "registry+https://github.com/rust-lang/crates.io-index"
111 @@ -460,6 +516,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
112 checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
113
114 [[package]]
115+ name = "der"
116+ version = "0.7.9"
117+ source = "registry+https://github.com/rust-lang/crates.io-index"
118+ checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
119+ dependencies = [
120+ "const-oid",
121+ "pem-rfc7468",
122+ "zeroize",
123+ ]
124+
125+ [[package]]
126 name = "deranged"
127 version = "0.3.11"
128 source = "registry+https://github.com/rust-lang/crates.io-index"
129 @@ -486,6 +553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
130 checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
131 dependencies = [
132 "block-buffer",
133+ "const-oid",
134 "crypto-common",
135 "subtle",
136 ]
137 @@ -508,12 +576,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
138 checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
139
140 [[package]]
141+ name = "ed25519"
142+ version = "2.2.3"
143+ source = "registry+https://github.com/rust-lang/crates.io-index"
144+ checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
145+ dependencies = [
146+ "pkcs8",
147+ "signature",
148+ ]
149+
150+ [[package]]
151+ name = "ed25519-dalek"
152+ version = "2.1.1"
153+ source = "registry+https://github.com/rust-lang/crates.io-index"
154+ checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
155+ dependencies = [
156+ "curve25519-dalek",
157+ "ed25519",
158+ "serde",
159+ "sha2",
160+ "subtle",
161+ "zeroize",
162+ ]
163+
164+ [[package]]
165 name = "either"
166 version = "1.13.0"
167 source = "registry+https://github.com/rust-lang/crates.io-index"
168 checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
169
170 [[package]]
171+ name = "email-encoding"
172+ version = "0.3.1"
173+ source = "registry+https://github.com/rust-lang/crates.io-index"
174+ checksum = "ea3d894bbbab314476b265f9b2d46bf24b123a36dd0e96b06a1b49545b9d9dcc"
175+ dependencies = [
176+ "base64 0.22.1",
177+ "memchr",
178+ ]
179+
180+ [[package]]
181 name = "email_address"
182 version = "0.2.9"
183 source = "registry+https://github.com/rust-lang/crates.io-index"
184 @@ -560,6 +662,18 @@ dependencies = [
185 ]
186
187 [[package]]
188+ name = "fastrand"
189+ version = "2.3.0"
190+ source = "registry+https://github.com/rust-lang/crates.io-index"
191+ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
192+
193+ [[package]]
194+ name = "fiat-crypto"
195+ version = "0.2.9"
196+ source = "registry+https://github.com/rust-lang/crates.io-index"
197+ checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
198+
199+ [[package]]
200 name = "flate2"
201 version = "1.0.33"
202 source = "registry+https://github.com/rust-lang/crates.io-index"
203 @@ -731,6 +845,10 @@ name = "hashbrown"
204 version = "0.14.5"
205 source = "registry+https://github.com/rust-lang/crates.io-index"
206 checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
207+ dependencies = [
208+ "ahash",
209+ "allocator-api2",
210+ ]
211
212 [[package]]
213 name = "heck"
214 @@ -780,9 +898,9 @@ dependencies = [
215
216 [[package]]
217 name = "hickory-resolver"
218- version = "0.24.1"
219+ version = "0.24.2"
220 source = "registry+https://github.com/rust-lang/crates.io-index"
221- checksum = "28757f23aa75c98f254cf0405e6d8c25b831b32921b050a66692427679b1f243"
222+ checksum = "0a2e2aba9c389ce5267d31cf1e4dace82390ae276b0b364ea55630b1fa1b44b4"
223 dependencies = [
224 "cfg-if",
225 "futures-util",
226 @@ -831,6 +949,141 @@ dependencies = [
227 ]
228
229 [[package]]
230+ name = "hostname"
231+ version = "0.4.0"
232+ source = "registry+https://github.com/rust-lang/crates.io-index"
233+ checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
234+ dependencies = [
235+ "cfg-if",
236+ "libc",
237+ "windows",
238+ ]
239+
240+ [[package]]
241+ name = "httpdate"
242+ version = "1.0.3"
243+ source = "registry+https://github.com/rust-lang/crates.io-index"
244+ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
245+
246+ [[package]]
247+ name = "icu_collections"
248+ version = "1.5.0"
249+ source = "registry+https://github.com/rust-lang/crates.io-index"
250+ checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
251+ dependencies = [
252+ "displaydoc",
253+ "yoke",
254+ "zerofrom",
255+ "zerovec",
256+ ]
257+
258+ [[package]]
259+ name = "icu_locid"
260+ version = "1.5.0"
261+ source = "registry+https://github.com/rust-lang/crates.io-index"
262+ checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
263+ dependencies = [
264+ "displaydoc",
265+ "litemap",
266+ "tinystr",
267+ "writeable",
268+ "zerovec",
269+ ]
270+
271+ [[package]]
272+ name = "icu_locid_transform"
273+ version = "1.5.0"
274+ source = "registry+https://github.com/rust-lang/crates.io-index"
275+ checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
276+ dependencies = [
277+ "displaydoc",
278+ "icu_locid",
279+ "icu_locid_transform_data",
280+ "icu_provider",
281+ "tinystr",
282+ "zerovec",
283+ ]
284+
285+ [[package]]
286+ name = "icu_locid_transform_data"
287+ version = "1.5.0"
288+ source = "registry+https://github.com/rust-lang/crates.io-index"
289+ checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
290+
291+ [[package]]
292+ name = "icu_normalizer"
293+ version = "1.5.0"
294+ source = "registry+https://github.com/rust-lang/crates.io-index"
295+ checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
296+ dependencies = [
297+ "displaydoc",
298+ "icu_collections",
299+ "icu_normalizer_data",
300+ "icu_properties",
301+ "icu_provider",
302+ "smallvec",
303+ "utf16_iter",
304+ "utf8_iter",
305+ "write16",
306+ "zerovec",
307+ ]
308+
309+ [[package]]
310+ name = "icu_normalizer_data"
311+ version = "1.5.0"
312+ source = "registry+https://github.com/rust-lang/crates.io-index"
313+ checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
314+
315+ [[package]]
316+ name = "icu_properties"
317+ version = "1.5.1"
318+ source = "registry+https://github.com/rust-lang/crates.io-index"
319+ checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
320+ dependencies = [
321+ "displaydoc",
322+ "icu_collections",
323+ "icu_locid_transform",
324+ "icu_properties_data",
325+ "icu_provider",
326+ "tinystr",
327+ "zerovec",
328+ ]
329+
330+ [[package]]
331+ name = "icu_properties_data"
332+ version = "1.5.0"
333+ source = "registry+https://github.com/rust-lang/crates.io-index"
334+ checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
335+
336+ [[package]]
337+ name = "icu_provider"
338+ version = "1.5.0"
339+ source = "registry+https://github.com/rust-lang/crates.io-index"
340+ checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
341+ dependencies = [
342+ "displaydoc",
343+ "icu_locid",
344+ "icu_provider_macros",
345+ "stable_deref_trait",
346+ "tinystr",
347+ "writeable",
348+ "yoke",
349+ "zerofrom",
350+ "zerovec",
351+ ]
352+
353+ [[package]]
354+ name = "icu_provider_macros"
355+ version = "1.5.0"
356+ source = "registry+https://github.com/rust-lang/crates.io-index"
357+ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
358+ dependencies = [
359+ "proc-macro2",
360+ "quote",
361+ "syn",
362+ ]
363+
364+ [[package]]
365 name = "idna"
366 version = "0.4.0"
367 source = "registry+https://github.com/rust-lang/crates.io-index"
368 @@ -851,6 +1104,27 @@ dependencies = [
369 ]
370
371 [[package]]
372+ name = "idna"
373+ version = "1.0.3"
374+ source = "registry+https://github.com/rust-lang/crates.io-index"
375+ checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
376+ dependencies = [
377+ "idna_adapter",
378+ "smallvec",
379+ "utf8_iter",
380+ ]
381+
382+ [[package]]
383+ name = "idna_adapter"
384+ version = "1.2.0"
385+ source = "registry+https://github.com/rust-lang/crates.io-index"
386+ checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
387+ dependencies = [
388+ "icu_normalizer",
389+ "icu_properties",
390+ ]
391+
392+ [[package]]
393 name = "indexmap"
394 version = "2.5.0"
395 source = "registry+https://github.com/rust-lang/crates.io-index"
396 @@ -931,6 +1205,9 @@ name = "lazy_static"
397 version = "1.5.0"
398 source = "registry+https://github.com/rust-lang/crates.io-index"
399 checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
400+ dependencies = [
401+ "spin 0.9.8",
402+ ]
403
404 [[package]]
405 name = "lazycell"
406 @@ -939,6 +1216,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
407 checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
408
409 [[package]]
410+ name = "lettre"
411+ version = "0.11.11"
412+ source = "registry+https://github.com/rust-lang/crates.io-index"
413+ checksum = "ab4c9a167ff73df98a5ecc07e8bf5ce90b583665da3d1762eb1f775ad4d0d6f5"
414+ dependencies = [
415+ "async-trait",
416+ "base64 0.22.1",
417+ "chumsky",
418+ "ed25519-dalek",
419+ "email-encoding",
420+ "email_address",
421+ "fastrand",
422+ "futures-io",
423+ "futures-util",
424+ "hostname 0.4.0",
425+ "httpdate",
426+ "idna 1.0.3",
427+ "mime",
428+ "nom",
429+ "percent-encoding",
430+ "quoted_printable",
431+ "rsa",
432+ "rustls 0.23.15",
433+ "rustls-pemfile 2.2.0",
434+ "rustls-pki-types",
435+ "sha2",
436+ "socket2",
437+ "tokio",
438+ "tokio-rustls 0.26.0",
439+ "url",
440+ "webpki-roots",
441+ ]
442+
443+ [[package]]
444 name = "libc"
445 version = "0.2.155"
446 source = "registry+https://github.com/rust-lang/crates.io-index"
447 @@ -955,6 +1266,12 @@ dependencies = [
448 ]
449
450 [[package]]
451+ name = "libm"
452+ version = "0.2.11"
453+ source = "registry+https://github.com/rust-lang/crates.io-index"
454+ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
455+
456+ [[package]]
457 name = "linked-hash-map"
458 version = "0.5.6"
459 source = "registry+https://github.com/rust-lang/crates.io-index"
460 @@ -967,6 +1284,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
461 checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
462
463 [[package]]
464+ name = "litemap"
465+ version = "0.7.4"
466+ source = "registry+https://github.com/rust-lang/crates.io-index"
467+ checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
468+
469+ [[package]]
470 name = "lock_api"
471 version = "0.4.12"
472 source = "registry+https://github.com/rust-lang/crates.io-index"
473 @@ -1078,11 +1401,14 @@ dependencies = [
474 "crossbeam-deque",
475 "email_address",
476 "futures",
477+ "hickory-resolver",
478+ "lettre",
479 "mail-auth",
480 "mail-builder",
481 "mail-parser",
482 "maildir",
483 "md5",
484+ "port_check",
485 "proxy-header",
486 "rustls 0.23.15",
487 "rustls-pemfile 2.2.0",
488 @@ -1133,6 +1459,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
489 checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
490
491 [[package]]
492+ name = "mime"
493+ version = "0.3.17"
494+ source = "registry+https://github.com/rust-lang/crates.io-index"
495+ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
496+
497+ [[package]]
498 name = "minimal-lexical"
499 version = "0.2.1"
500 source = "registry+https://github.com/rust-lang/crates.io-index"
501 @@ -1195,12 +1527,59 @@ dependencies = [
502 ]
503
504 [[package]]
505+ name = "num-bigint-dig"
506+ version = "0.8.4"
507+ source = "registry+https://github.com/rust-lang/crates.io-index"
508+ checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
509+ dependencies = [
510+ "byteorder",
511+ "lazy_static",
512+ "libm",
513+ "num-integer",
514+ "num-iter",
515+ "num-traits",
516+ "rand",
517+ "smallvec",
518+ "zeroize",
519+ ]
520+
521+ [[package]]
522 name = "num-conv"
523 version = "0.1.0"
524 source = "registry+https://github.com/rust-lang/crates.io-index"
525 checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
526
527 [[package]]
528+ name = "num-integer"
529+ version = "0.1.46"
530+ source = "registry+https://github.com/rust-lang/crates.io-index"
531+ checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
532+ dependencies = [
533+ "num-traits",
534+ ]
535+
536+ [[package]]
537+ name = "num-iter"
538+ version = "0.1.45"
539+ source = "registry+https://github.com/rust-lang/crates.io-index"
540+ checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
541+ dependencies = [
542+ "autocfg",
543+ "num-integer",
544+ "num-traits",
545+ ]
546+
547+ [[package]]
548+ name = "num-traits"
549+ version = "0.2.19"
550+ source = "registry+https://github.com/rust-lang/crates.io-index"
551+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
552+ dependencies = [
553+ "autocfg",
554+ "libm",
555+ ]
556+
557+ [[package]]
558 name = "object"
559 version = "0.36.2"
560 source = "registry+https://github.com/rust-lang/crates.io-index"
561 @@ -1261,6 +1640,15 @@ dependencies = [
562 ]
563
564 [[package]]
565+ name = "pem-rfc7468"
566+ version = "0.7.0"
567+ source = "registry+https://github.com/rust-lang/crates.io-index"
568+ checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
569+ dependencies = [
570+ "base64ct",
571+ ]
572+
573+ [[package]]
574 name = "percent-encoding"
575 version = "2.3.1"
576 source = "registry+https://github.com/rust-lang/crates.io-index"
577 @@ -1279,12 +1667,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
578 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
579
580 [[package]]
581+ name = "pkcs1"
582+ version = "0.7.5"
583+ source = "registry+https://github.com/rust-lang/crates.io-index"
584+ checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
585+ dependencies = [
586+ "der",
587+ "pkcs8",
588+ "spki",
589+ ]
590+
591+ [[package]]
592+ name = "pkcs8"
593+ version = "0.10.2"
594+ source = "registry+https://github.com/rust-lang/crates.io-index"
595+ checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
596+ dependencies = [
597+ "der",
598+ "spki",
599+ ]
600+
601+ [[package]]
602 name = "pkg-config"
603 version = "0.3.30"
604 source = "registry+https://github.com/rust-lang/crates.io-index"
605 checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
606
607 [[package]]
608+ name = "port_check"
609+ version = "0.2.1"
610+ source = "registry+https://github.com/rust-lang/crates.io-index"
611+ checksum = "2110609fb863cdb367d4e69d6c43c81ba6a8c7d18e80082fe9f3ef16b23afeed"
612+
613+ [[package]]
614 name = "powerfmt"
615 version = "0.2.0"
616 source = "registry+https://github.com/rust-lang/crates.io-index"
617 @@ -1325,6 +1740,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
618 checksum = "dc1493f63ddddfba840c3169e997c2905d09538ace72d64e84af6324c6e0e065"
619
620 [[package]]
621+ name = "psm"
622+ version = "0.1.24"
623+ source = "registry+https://github.com/rust-lang/crates.io-index"
624+ checksum = "200b9ff220857e53e184257720a14553b2f4aa02577d2ed9842d45d4b9654810"
625+ dependencies = [
626+ "cc",
627+ ]
628+
629+ [[package]]
630 name = "quick-error"
631 version = "1.2.3"
632 source = "registry+https://github.com/rust-lang/crates.io-index"
633 @@ -1428,7 +1852,7 @@ version = "0.7.0"
634 source = "registry+https://github.com/rust-lang/crates.io-index"
635 checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00"
636 dependencies = [
637- "hostname",
638+ "hostname 0.3.1",
639 "quick-error",
640 ]
641
642 @@ -1463,6 +1887,26 @@ dependencies = [
643 ]
644
645 [[package]]
646+ name = "rsa"
647+ version = "0.9.7"
648+ source = "registry+https://github.com/rust-lang/crates.io-index"
649+ checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
650+ dependencies = [
651+ "const-oid",
652+ "digest",
653+ "num-bigint-dig",
654+ "num-integer",
655+ "num-traits",
656+ "pkcs1",
657+ "pkcs8",
658+ "rand_core",
659+ "signature",
660+ "spki",
661+ "subtle",
662+ "zeroize",
663+ ]
664+
665+ [[package]]
666 name = "rustc-demangle"
667 version = "0.1.24"
668 source = "registry+https://github.com/rust-lang/crates.io-index"
669 @@ -1475,6 +1919,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
670 checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
671
672 [[package]]
673+ name = "rustc_version"
674+ version = "0.4.1"
675+ source = "registry+https://github.com/rust-lang/crates.io-index"
676+ checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
677+ dependencies = [
678+ "semver",
679+ ]
680+
681+ [[package]]
682 name = "rustix"
683 version = "0.38.34"
684 source = "registry+https://github.com/rust-lang/crates.io-index"
685 @@ -1508,6 +1961,7 @@ dependencies = [
686 "aws-lc-rs",
687 "log",
688 "once_cell",
689+ "ring 0.17.8",
690 "rustls-pki-types",
691 "rustls-webpki 0.102.8",
692 "subtle",
693 @@ -1583,6 +2037,12 @@ dependencies = [
694 ]
695
696 [[package]]
697+ name = "semver"
698+ version = "1.0.24"
699+ source = "registry+https://github.com/rust-lang/crates.io-index"
700+ checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
701+
702+ [[package]]
703 name = "serde"
704 version = "1.0.213"
705 source = "registry+https://github.com/rust-lang/crates.io-index"
706 @@ -1635,6 +2095,17 @@ dependencies = [
707 ]
708
709 [[package]]
710+ name = "sha2"
711+ version = "0.10.8"
712+ source = "registry+https://github.com/rust-lang/crates.io-index"
713+ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
714+ dependencies = [
715+ "cfg-if",
716+ "cpufeatures",
717+ "digest",
718+ ]
719+
720+ [[package]]
721 name = "sharded-slab"
722 version = "0.1.7"
723 source = "registry+https://github.com/rust-lang/crates.io-index"
724 @@ -1659,6 +2130,16 @@ dependencies = [
725 ]
726
727 [[package]]
728+ name = "signature"
729+ version = "2.2.0"
730+ source = "registry+https://github.com/rust-lang/crates.io-index"
731+ checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
732+ dependencies = [
733+ "digest",
734+ "rand_core",
735+ ]
736+
737+ [[package]]
738 name = "simd-adler32"
739 version = "0.3.7"
740 source = "registry+https://github.com/rust-lang/crates.io-index"
741 @@ -1711,6 +2192,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
742 checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
743
744 [[package]]
745+ name = "spki"
746+ version = "0.7.3"
747+ source = "registry+https://github.com/rust-lang/crates.io-index"
748+ checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
749+ dependencies = [
750+ "base64ct",
751+ "der",
752+ ]
753+
754+ [[package]]
755+ name = "stable_deref_trait"
756+ version = "1.2.0"
757+ source = "registry+https://github.com/rust-lang/crates.io-index"
758+ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
759+
760+ [[package]]
761+ name = "stacker"
762+ version = "0.1.15"
763+ source = "registry+https://github.com/rust-lang/crates.io-index"
764+ checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
765+ dependencies = [
766+ "cc",
767+ "cfg-if",
768+ "libc",
769+ "psm",
770+ "winapi",
771+ ]
772+
773+ [[package]]
774 name = "stringprep"
775 version = "0.1.5"
776 source = "registry+https://github.com/rust-lang/crates.io-index"
777 @@ -1745,6 +2255,17 @@ dependencies = [
778 ]
779
780 [[package]]
781+ name = "synstructure"
782+ version = "0.13.1"
783+ source = "registry+https://github.com/rust-lang/crates.io-index"
784+ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
785+ dependencies = [
786+ "proc-macro2",
787+ "quote",
788+ "syn",
789+ ]
790+
791+ [[package]]
792 name = "thiserror"
793 version = "1.0.65"
794 source = "registry+https://github.com/rust-lang/crates.io-index"
795 @@ -1794,6 +2315,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
796 checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
797
798 [[package]]
799+ name = "tinystr"
800+ version = "0.7.6"
801+ source = "registry+https://github.com/rust-lang/crates.io-index"
802+ checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
803+ dependencies = [
804+ "displaydoc",
805+ "zerovec",
806+ ]
807+
808+ [[package]]
809 name = "tinyvec"
810 version = "1.8.0"
811 source = "registry+https://github.com/rust-lang/crates.io-index"
812 @@ -2036,6 +2567,18 @@ dependencies = [
813 ]
814
815 [[package]]
816+ name = "utf16_iter"
817+ version = "1.0.5"
818+ source = "registry+https://github.com/rust-lang/crates.io-index"
819+ checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
820+
821+ [[package]]
822+ name = "utf8_iter"
823+ version = "1.0.4"
824+ source = "registry+https://github.com/rust-lang/crates.io-index"
825+ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
826+
827+ [[package]]
828 name = "utf8parse"
829 version = "0.2.2"
830 source = "registry+https://github.com/rust-lang/crates.io-index"
831 @@ -2125,6 +2668,15 @@ dependencies = [
832 ]
833
834 [[package]]
835+ name = "webpki-roots"
836+ version = "0.26.7"
837+ source = "registry+https://github.com/rust-lang/crates.io-index"
838+ checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e"
839+ dependencies = [
840+ "rustls-pki-types",
841+ ]
842+
843+ [[package]]
844 name = "which"
845 version = "4.4.2"
846 source = "registry+https://github.com/rust-lang/crates.io-index"
847 @@ -2165,6 +2717,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
848 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
849
850 [[package]]
851+ name = "windows"
852+ version = "0.52.0"
853+ source = "registry+https://github.com/rust-lang/crates.io-index"
854+ checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
855+ dependencies = [
856+ "windows-core",
857+ "windows-targets 0.52.6",
858+ ]
859+
860+ [[package]]
861+ name = "windows-core"
862+ version = "0.52.0"
863+ source = "registry+https://github.com/rust-lang/crates.io-index"
864+ checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
865+ dependencies = [
866+ "windows-targets 0.52.6",
867+ ]
868+
869+ [[package]]
870 name = "windows-sys"
871 version = "0.48.0"
872 source = "registry+https://github.com/rust-lang/crates.io-index"
873 @@ -2323,6 +2894,42 @@ dependencies = [
874 ]
875
876 [[package]]
877+ name = "write16"
878+ version = "1.0.0"
879+ source = "registry+https://github.com/rust-lang/crates.io-index"
880+ checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
881+
882+ [[package]]
883+ name = "writeable"
884+ version = "0.5.5"
885+ source = "registry+https://github.com/rust-lang/crates.io-index"
886+ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
887+
888+ [[package]]
889+ name = "yoke"
890+ version = "0.7.5"
891+ source = "registry+https://github.com/rust-lang/crates.io-index"
892+ checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
893+ dependencies = [
894+ "serde",
895+ "stable_deref_trait",
896+ "yoke-derive",
897+ "zerofrom",
898+ ]
899+
900+ [[package]]
901+ name = "yoke-derive"
902+ version = "0.7.5"
903+ source = "registry+https://github.com/rust-lang/crates.io-index"
904+ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
905+ dependencies = [
906+ "proc-macro2",
907+ "quote",
908+ "syn",
909+ "synstructure",
910+ ]
911+
912+ [[package]]
913 name = "zerocopy"
914 version = "0.7.35"
915 source = "registry+https://github.com/rust-lang/crates.io-index"
916 @@ -2344,6 +2951,27 @@ dependencies = [
917 ]
918
919 [[package]]
920+ name = "zerofrom"
921+ version = "0.1.5"
922+ source = "registry+https://github.com/rust-lang/crates.io-index"
923+ checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
924+ dependencies = [
925+ "zerofrom-derive",
926+ ]
927+
928+ [[package]]
929+ name = "zerofrom-derive"
930+ version = "0.1.5"
931+ source = "registry+https://github.com/rust-lang/crates.io-index"
932+ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
933+ dependencies = [
934+ "proc-macro2",
935+ "quote",
936+ "syn",
937+ "synstructure",
938+ ]
939+
940+ [[package]]
941 name = "zeroize"
942 version = "1.8.1"
943 source = "registry+https://github.com/rust-lang/crates.io-index"
944 @@ -2364,6 +2992,28 @@ dependencies = [
945 ]
946
947 [[package]]
948+ name = "zerovec"
949+ version = "0.10.4"
950+ source = "registry+https://github.com/rust-lang/crates.io-index"
951+ checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
952+ dependencies = [
953+ "yoke",
954+ "zerofrom",
955+ "zerovec-derive",
956+ ]
957+
958+ [[package]]
959+ name = "zerovec-derive"
960+ version = "0.10.3"
961+ source = "registry+https://github.com/rust-lang/crates.io-index"
962+ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
963+ dependencies = [
964+ "proc-macro2",
965+ "quote",
966+ "syn",
967+ ]
968+
969+ [[package]]
970 name = "zip"
971 version = "2.2.0"
972 source = "registry+https://github.com/rust-lang/crates.io-index"
973 diff --git a/maitred/Cargo.toml b/maitred/Cargo.toml
974index 5bc5b5f..a3426ff 100644
975--- a/maitred/Cargo.toml
976+++ b/maitred/Cargo.toml
977 @@ -10,6 +10,7 @@ bytes = "1.8.0"
978 crossbeam-deque = { version = "0.8.5", optional = true }
979 email_address = "0.2.9"
980 futures = "0.3.31"
981+ hickory-resolver = { version = "0.24.2", optional = true }
982 mail-auth = { version = "0.5.1", features = ["ring", "rustls-pemfile"] }
983 mail-builder = "0.3.2"
984 mail-parser = { version = "0.9.4", features = ["serde", "serde_support"] }
985 @@ -28,12 +29,23 @@ tokio-util = { version = "0.7.12", features = ["full"], optional = true }
986 tracing = { version = "0.1.40", features = ["log"] }
987 url = "2.5.2"
988
989+ [dependencies.lettre]
990+ version = "0.11.11"
991+ features = ["dkim", "rustls-tls", "tokio1", "tokio1-rustls-tls", "builder", "hostname", "pool", "smtp-transport"]
992+ optional = true
993+ default-features = false
994+
995 [dev-dependencies]
996+ port_check = "0.2.1"
997 tracing-subscriber = "0.3.18"
998
999 [features]
1000 default = []
1001- full = ["server", "auth"]
1002+ full = ["auth", "client", "server"]
1003+ auth = [
1004+ "base64",
1005+ "server"
1006+ ]
1007 server = [
1008 "crossbeam-deque",
1009 "rustls",
1010 @@ -43,7 +55,7 @@ server = [
1011 "tokio-stream",
1012 "tokio-util"
1013 ]
1014- auth = [
1015- "base64",
1016- "server"
1017+ client = [
1018+ "hickory-resolver",
1019+ "lettre"
1020 ]
1021 diff --git a/maitred/src/auth.rs b/maitred/src/auth.rs
1022index 4bc6eea..9094154 100644
1023--- a/maitred/src/auth.rs
1024+++ b/maitred/src/auth.rs
1025 @@ -63,7 +63,7 @@ pub trait PlainAuth: Sync + Send {
1026 authcid: &str,
1027 authzid: &str,
1028 passwd: &str,
1029- ) -> Result<(), AuthError>;
1030+ ) -> Result<String, AuthError>;
1031 }
1032
1033 /// Convenience function implementing PlainAuth
1034 @@ -83,9 +83,12 @@ where
1035 authcid: &str,
1036 authzid: &str,
1037 passwd: &str,
1038- ) -> Result<(), AuthError> {
1039+ ) -> Result<String, AuthError> {
1040 let f = (self.0)(authcid, authzid, passwd);
1041- f.await
1042+ match f.await {
1043+ Ok(_) => Ok(authcid.to_string()),
1044+ Err(e) => Err(e),
1045+ }
1046 }
1047 }
1048
1049 diff --git a/maitred/src/client.rs b/maitred/src/client.rs
1050new file mode 100644
1051index 0000000..a1956ab
1052--- /dev/null
1053+++ b/maitred/src/client.rs
1054 @@ -0,0 +1,130 @@
1055+ use hickory_resolver::error::ResolveError;
1056+ use hickory_resolver::proto::rr::rdata::MX;
1057+ use hickory_resolver::system_conf::read_system_conf;
1058+ use hickory_resolver::TokioAsyncResolver;
1059+ use lettre::error::Error as LettreError;
1060+ use lettre::{
1061+ address::AddressError, message::Mailbox, Address as LettreAddress, AsyncSmtpTransport,
1062+ Message as LettreMessage,
1063+ };
1064+ use mail_parser::{Addr, Message};
1065+
1066+ #[derive(Debug, thiserror::Error)]
1067+ pub enum Error {
1068+ #[error("Message does not contain a TO field")]
1069+ NoToAddress,
1070+ #[error("Message does not contain a FROM field")]
1071+ NoFromAddress,
1072+ #[error("Cannot parse email address: {0}")]
1073+ Address(#[from] AddressError),
1074+ #[error("Client error: {0}")]
1075+ Lettre(#[from] LettreError),
1076+ #[error("DNS Resolution: {0}")]
1077+ Resolution(#[from] ResolveError),
1078+ }
1079+
1080+ pub struct Client {}
1081+
1082+ async fn resolve_mx_record(domain: &str) -> Result<Vec<MX>, ResolveError> {
1083+ let (cfg, opts) = read_system_conf()?;
1084+ let resolver = TokioAsyncResolver::tokio(cfg, opts);
1085+ let response = resolver.mx_lookup(domain).await?;
1086+ let mut records: Vec<MX> = response.iter().cloned().collect();
1087+ records.sort_by_key(|record| record.preference());
1088+ Ok(records)
1089+ }
1090+
1091+ fn read_mailbox(input: &Addr<'_>) -> Result<Mailbox, Error> {
1092+ let addr_str = input
1093+ .address()
1094+ .map_or(Err(Error::NoFromAddress), |x| Ok(x.to_string()))?;
1095+ Ok(Mailbox::new(
1096+ input.name().map(|name| name.to_string()),
1097+ addr_str.parse::<LettreAddress>()?,
1098+ ))
1099+ }
1100+
1101+ pub async fn send(recipient: &Mailbox, message: Message<'_>) -> Result<(), Error> {
1102+ let from = message.from().map_or(Err(Error::NoFromAddress), |from| {
1103+ from.first()
1104+ .map_or(Err(Error::NoFromAddress), |from| read_mailbox(from))
1105+ })?;
1106+ let mx_records = resolve_mx_record(from.email.domain()).await?;
1107+ for record in mx_records {
1108+ tracing::info!("Attempting to deliver message to domain: {}", record);
1109+ // let transport = AsyncSmtpTransport::relay(&record.exchange().to_string())
1110+ // .unwrap()
1111+ // .build();
1112+ // // if !transport.test_connection().await? {
1113+ // tracing::info!("Cannot establish connection to client");
1114+ // continue;
1115+ // }
1116+ }
1117+ todo!()
1118+ }
1119+
1120+ pub fn recipients(message: Message<'_>) -> Result<Vec<Mailbox>, Error> {
1121+ let to = message.to().map_or(Err(Error::NoToAddress), |to| {
1122+ to.first()
1123+ .map_or(Err(Error::NoToAddress), |to| read_mailbox(to))
1124+ })?;
1125+ let cc = message
1126+ .cc()
1127+ .map(|cc| {
1128+ let values: Result<Vec<Mailbox>, Error> =
1129+ cc.iter().try_fold(Vec::new(), |mut accm, x| {
1130+ let mailbox = read_mailbox(x)?;
1131+ accm.push(mailbox);
1132+ Ok(accm)
1133+ });
1134+ values
1135+ })
1136+ .transpose()?;
1137+ Ok([Vec::from_iter([to]), cc.unwrap_or_default()].concat())
1138+ }
1139+
1140+ #[cfg(test)]
1141+ mod test {
1142+ use std::time::Duration;
1143+
1144+ use super::*;
1145+ use mail_parser::MessageParser;
1146+ use port_check::free_local_ipv4_port;
1147+
1148+ const TEST_EMAIL: &str = r#"From: hello@ayllu-forge.org
1149+ To: dev@ayllu-dev.local
1150+ Cc: Fuu Bar <me@example.org>
1151+ Subject: [PATCH] add delivery parameters for mail module in db crate
1152+ Date: Mon, 23 Dec 2024 18:49:34 +0100
1153+ Message-ID: <20241223174934.5903-1-hello@ayllu-forge.org>
1154+ X-Mailer: git-send-email 2.47.1
1155+ MIME-Version: 1.0
1156+ Content-Transfer-Encoding: 8bit
1157+
1158+ From: Fuu Bar <me@example.org>
1159+
1160+ ---
1161+ ayllu-mail/src/delivery.rs | 12 +++++-----
1162+
1163+ TRUNCATED
1164+ "#;
1165+
1166+ #[test]
1167+ fn recipient_parsing() {
1168+ let parser = MessageParser::new();
1169+ let message = parser.parse(TEST_EMAIL).unwrap();
1170+ let mailboxes = recipients(message).unwrap();
1171+ assert!(mailboxes.len() == 2);
1172+ assert!(mailboxes.first().unwrap().email.domain() == "ayllu-dev.local");
1173+ assert!(mailboxes.get(1).unwrap().email.domain() == "example.org");
1174+ }
1175+
1176+ #[tokio::test]
1177+ async fn client_server() {
1178+ let test_addr = format!("127.0.0.1:{}", free_local_ipv4_port().unwrap());
1179+ tokio::task::spawn(async move {
1180+ let mut test_server = crate::Server::default().address(&test_addr);
1181+ test_server.listen().await.unwrap();
1182+ });
1183+ }
1184+ }
1185 diff --git a/maitred/src/expand.rs b/maitred/src/expand.rs
1186index 4bb9f9b..b810ecb 100644
1187--- a/maitred/src/expand.rs
1188+++ b/maitred/src/expand.rs
1189 @@ -32,7 +32,7 @@ impl Into<Response<String>> for ExpansionError {
1190 /// only be called with proper authentication otherwise it could be used to
1191 /// harvest e-mail addresses.
1192 #[async_trait]
1193- pub trait Expansion {
1194+ pub trait Expansion: Sync + Send {
1195 /// Expand the group into an array of members
1196 async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError>;
1197 }
1198 @@ -54,13 +54,13 @@ pub trait Expansion {
1199 /// ```
1200 pub struct ExpansionFunc<F, T>(pub F)
1201 where
1202- F: Fn(&str) -> T + Sync,
1203+ F: Fn(&str) -> T + Sync + Send,
1204 T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send;
1205
1206 #[async_trait]
1207 impl<F, T> Expansion for ExpansionFunc<F, T>
1208 where
1209- F: Fn(&str) -> T + Sync,
1210+ F: Fn(&str) -> T + Sync + Send,
1211 T: Future<Output = Result<Vec<EmailAddress>, ExpansionError>> + Send,
1212 {
1213 async fn expand(&self, name: &str) -> Result<Vec<EmailAddress>, ExpansionError> {
1214 diff --git a/maitred/src/lib.rs b/maitred/src/lib.rs
1215index ec4e5f2..7b045b9 100644
1216--- a/maitred/src/lib.rs
1217+++ b/maitred/src/lib.rs
1218 @@ -102,6 +102,9 @@ pub use milter::{Milter, MilterError, MilterFunc};
1219 pub mod verify;
1220 #[doc(inline)]
1221 pub use verify::{Verify, VerifyError, VerifyFunc};
1222+ /// DNS Resolution Helper
1223+ #[cfg(feature = "client")]
1224+ pub mod client;
1225
1226 /// Generate a single smtp_response
1227 macro_rules! smtp_response {
1228 diff --git a/maitred/src/server.rs b/maitred/src/server.rs
1229index 3ece38c..47acf14 100644
1230--- a/maitred/src/server.rs
1231+++ b/maitred/src/server.rs
1232 @@ -272,7 +272,7 @@ impl Server {
1233 } => {
1234 conn.send(initial_response).await?;
1235 match conn.next().await {
1236- Some(Ok(Command::Payload(payload))) => match cb(payload) {
1237+ Some(Ok(Command::Payload(payload))) => match cb.call(&payload) {
1238 crate::session::Action::Send(response) => {
1239 conn.send(response).await?;
1240 }
1241 @@ -304,7 +304,7 @@ impl Server {
1242 .plain_auth
1243 .as_ref()
1244 .expect("authentication not available");
1245- match cb(plain_auth.authenticate(&authcid, &authzid, &password).await) {
1246+ match cb.call(plain_auth.authenticate(&authcid, &authzid, &password).await) {
1247 crate::session::Action::Send(response) => {
1248 conn.send(response).await?;
1249 }
1250 @@ -316,7 +316,7 @@ impl Server {
1251 .verification
1252 .as_ref()
1253 .expect("verification not available");
1254- match cb(verification.verify(&address).await) {
1255+ match cb.call(verification.verify(&address).await) {
1256 crate::session::Action::Send(response) => {
1257 conn.send(response).await?;
1258 }
1259 @@ -328,7 +328,7 @@ impl Server {
1260 .list_expansion
1261 .as_ref()
1262 .expect("expansion not available");
1263- match cb(expansion.expand(&address).await) {
1264+ match cb.call(expansion.expand(&address).await) {
1265 crate::session::Action::Send(response) => {
1266 conn.send(response).await?;
1267 }
1268 @@ -680,4 +680,12 @@ mod test {
1269 .first()
1270 .is_some_and(|rcpt_to| rcpt_to.email() == "baz@qux.com"));
1271 }
1272+
1273+ #[tokio::test]
1274+ async fn server_is_send() {
1275+ let server = Server::default();
1276+ tokio::task::spawn(async {
1277+ assert!(server.address("0.0.0.0:0").listen().await.is_err());
1278+ });
1279+ }
1280 }
1281 diff --git a/maitred/src/session.rs b/maitred/src/session.rs
1282index 2f1008b..280a22f 100644
1283--- a/maitred/src/session.rs
1284+++ b/maitred/src/session.rs
1285 @@ -1,7 +1,7 @@
1286 use std::fmt::Display;
1287 use std::str::FromStr;
1288+ use std::sync::{Arc, Mutex};
1289
1290- use bytes::Bytes;
1291 use email_address::EmailAddress;
1292
1293 use mail_parser::{Message, MessageParser};
1294 @@ -84,6 +84,121 @@ where
1295
1296 impl<T> Eq for Response<T> where T: Display {}
1297
1298+ #[derive(Clone, Default)]
1299+ struct Capabilities {
1300+ mode: Option<Mode>,
1301+ capabilities: u32,
1302+ }
1303+
1304+ impl Capabilities {
1305+ pub fn enabled(&self, capability: u32) -> bool {
1306+ self.mode
1307+ .as_ref()
1308+ .is_some_and(|mode| matches!(mode, Mode::Extended))
1309+ && self.capabilities & capability != 0
1310+ }
1311+ }
1312+
1313+ pub struct SetMessage {
1314+ inner: Arc<Mutex<Inner>>,
1315+ capabilities: Capabilities,
1316+ }
1317+
1318+ impl SetMessage {
1319+ /// checks if 8BITMIME is supported
1320+ fn check_body(&self, body: &[u8]) -> Result<(), Response<String>> {
1321+ if !self.capabilities.enabled(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() {
1322+ return Err(smtp_response!(
1323+ 500,
1324+ 0,
1325+ 0,
1326+ 0,
1327+ "Non ASCII characters found in message body"
1328+ ));
1329+ }
1330+ Ok(())
1331+ }
1332+ /// Called each time a message is ready for processing, will do spf
1333+ /// validation if it is configured.
1334+ fn accept_payload(&self, inner: &Inner, payload: &[u8]) -> Action {
1335+ if inner.rcpt_to.is_none() {
1336+ return Action::Send(smtp_response!(500, 0, 0, 0, "RCPT TO is missing"));
1337+ }
1338+ if inner.hostname.is_none() {
1339+ return Action::Send(smtp_response!(500, 0, 0, 0, "Hostname is missing"));
1340+ }
1341+ // let copied = payload.to_vec();
1342+ if let Err(response) = self.check_body(payload) {
1343+ return Action::Send(response);
1344+ };
1345+ let parser = MessageParser::new();
1346+ match parser.parse(payload) {
1347+ Some(message) => Action::Envelope {
1348+ initial_response: smtp_response!(250, 0, 0, 0, "OK"),
1349+ envelope: Envelope {
1350+ body: message.into_owned(),
1351+ // FIXME
1352+ mail_from: inner.mail_from.clone().unwrap(),
1353+ rcpt_to: inner.rcpt_to.clone().unwrap(),
1354+ hostname: inner.hostname.clone().unwrap(),
1355+ },
1356+ },
1357+ None => Action::Send(smtp_response!(500, 0, 0, 0, "Cannot parse message payload")),
1358+ }
1359+ }
1360+
1361+ pub fn call(&self, message: &[u8]) -> Action {
1362+ let inner = self.inner.lock().unwrap();
1363+ self.accept_payload(&inner, message)
1364+ }
1365+ }
1366+
1367+ pub struct PlainAuth(Arc<Mutex<Inner>>);
1368+
1369+ impl PlainAuth {
1370+ pub fn call(&self, auth_response: Result<String, AuthError>) -> Action {
1371+ match auth_response {
1372+ Ok(authcid) => {
1373+ tracing::info!("Successfully Authenticated");
1374+ let mut inner = self.0.lock().unwrap();
1375+ inner.authenticated_id = Some(authcid.clone());
1376+ Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
1377+ }
1378+ Err(e) => Action::Send(e.into()),
1379+ }
1380+ }
1381+ }
1382+
1383+ pub struct Verify;
1384+
1385+ impl Verify {
1386+ pub fn call(&self, verify_response: Result<(), VerifyError>) -> Action {
1387+ match verify_response {
1388+ Ok(_) => Action::Send(smtp_response!(200, 0, 0, 0, "OK")),
1389+ Err(e) => Action::Send(e.into()),
1390+ }
1391+ }
1392+ }
1393+
1394+ pub struct Expand;
1395+
1396+ impl Expand {
1397+ pub fn call(&self, addresses: Result<Vec<EmailAddress>, ExpansionError>) -> Action {
1398+ match addresses {
1399+ Ok(addresses) => {
1400+ let mut responses = vec![smtp_response!(250, 0, 0, 0, "OK")];
1401+ responses.extend(
1402+ addresses
1403+ .iter()
1404+ .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())),
1405+ );
1406+ Action::SendMany(responses)
1407+ }
1408+ Err(e) => Action::Send(e.into()),
1409+ }
1410+ }
1411+ }
1412+
1413 /// An Envelope containing an e-mail message created from the session.
1414 #[derive(Clone, Debug)]
1415 pub struct Envelope {
1416 @@ -94,12 +209,12 @@ pub struct Envelope {
1417 }
1418
1419 /// Action for the server implementor to take probably asynchronously.
1420- pub enum Action<'a> {
1421+ pub enum Action {
1422 Send(Response<String>),
1423 SendMany(Vec<Response<String>>),
1424 Message {
1425 initial_response: Response<String>,
1426- cb: Box<dyn FnOnce(Bytes) -> Action<'a> + 'a>,
1427+ cb: SetMessage,
1428 },
1429 Envelope {
1430 initial_response: Response<String>,
1431 @@ -109,21 +224,21 @@ pub enum Action<'a> {
1432 authcid: String,
1433 authzid: String,
1434 password: String,
1435- cb: Box<dyn FnOnce(Result<(), AuthError>) -> Action<'a> + 'a>,
1436+ cb: PlainAuth,
1437 },
1438 Verify {
1439 address: EmailAddress,
1440- cb: Box<dyn FnOnce(Result<(), VerifyError>) -> Action<'a> + 'a>,
1441+ cb: Verify,
1442 },
1443 Expand {
1444 address: String,
1445- cb: Box<dyn FnOnce(Result<Vec<EmailAddress>, ExpansionError>) -> Action<'a> + 'a>,
1446+ cb: Expand,
1447 },
1448 StartTls(Response<String>),
1449 Quit(Response<String>),
1450 }
1451
1452- impl Display for Action<'_> {
1453+ impl Display for Action {
1454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1455 match self {
1456 Action::Send(response) => match response {
1457 @@ -156,8 +271,7 @@ impl Display for Action<'_> {
1458 envelope,
1459 } => f.write_fmt(format_args!(
1460 "Envelope: {:?}, {}",
1461- initial_response,
1462- envelope.mail_from.to_string()
1463+ initial_response, envelope.mail_from
1464 )),
1465 Action::PlainAuth {
1466 authcid,
1467 @@ -181,10 +295,6 @@ impl Display for Action<'_> {
1468 }
1469 }
1470
1471- /// Result generated as part of an SMTP session, an Err indicates a session
1472- /// level error that will be returned to the client.
1473- // pub type Result = StdResult<Action, Response<String>>;
1474-
1475 /// If the session was started with HELO or ELHO.
1476 #[derive(Clone)]
1477 enum Mode {
1478 @@ -269,10 +379,8 @@ struct Flags {
1479 expn: bool,
1480 }
1481
1482- /// State machine that corresponds to a single SMTP session, calls to next
1483- /// return actions that the caller is expected to implement in a transport.
1484- #[derive(Clone)]
1485- pub struct Session {
1486+ #[derive(Clone, Default)]
1487+ struct Inner {
1488 /// mailto address
1489 mail_from: Option<EmailAddress>,
1490 /// rcpt address
1491 @@ -284,6 +392,39 @@ pub struct Session {
1492 // previously ran commands
1493 // TODO pipeline still partially broken
1494 history: Vec<Request<String>>,
1495+ spf_verified_host: Option<String>,
1496+ authenticated_id: Option<String>,
1497+ }
1498+
1499+ impl Inner {
1500+ /// Reset the connection to it's default state but after a HELO/ELHO has
1501+ /// been issued successfully.
1502+ pub fn reset(&mut self) {
1503+ self.mail_from = None;
1504+ self.rcpt_to = None;
1505+ // self.hostname = None;
1506+ self.initialized = None;
1507+ self.history = Vec::new();
1508+ self.spf_verified_host = None;
1509+ }
1510+ }
1511+
1512+ /// State machine that corresponds to a single SMTP session, calls to next
1513+ /// return actions that the caller is expected to implement in a transport.
1514+ #[derive(Clone)]
1515+ pub struct Session {
1516+ // /// mailto address
1517+ // mail_from: Option<EmailAddress>,
1518+ // /// rcpt address
1519+ // rcpt_to: Option<Vec<EmailAddress>>,
1520+ // /// hostname per HELO
1521+ // hostname: Option<Host>,
1522+
1523+ // initialized: Option<Mode>,
1524+ // // previously ran commands
1525+ // // TODO pipeline still partially broken
1526+ // history: Vec<Request<String>>,
1527+ inner: Arc<Mutex<Inner>>,
1528
1529 // session opts
1530 our_hostname: Option<String>, // required
1531 @@ -293,27 +434,19 @@ pub struct Session {
1532 greeting: String,
1533 tls_active: bool,
1534
1535- spf_verified_host: Option<String>,
1536- authenticated_id: Option<String>,
1537 flags: Flags,
1538 }
1539
1540 impl Default for Session {
1541 fn default() -> Self {
1542 Session {
1543- mail_from: None,
1544- rcpt_to: None,
1545- hostname: None,
1546- initialized: None,
1547- history: Vec::new(),
1548+ inner: Arc::new(Mutex::new(Inner::default())),
1549 our_hostname: None,
1550 maximum_size: DEFAULT_MAXIMUM_MESSAGE_SIZE,
1551 capabilities: DEFAULT_CAPABILITIES,
1552 help_banner: DEFAULT_HELP_BANNER.to_string(),
1553 greeting: DEFAULT_GREETING.to_string(),
1554 tls_active: false,
1555- spf_verified_host: None,
1556- authenticated_id: None,
1557 flags: Flags::default(),
1558 }
1559 }
1560 @@ -372,17 +505,6 @@ impl Session {
1561 self
1562 }
1563
1564- /// Reset the connection to it's default state but after a HELO/ELHO has
1565- /// been issued successfully.
1566- pub fn reset(&mut self) {
1567- self.mail_from = None;
1568- self.rcpt_to = None;
1569- // FIXME: is the hostname reset?
1570- // self.hostname = None;
1571- self.history = Vec::new();
1572- self.spf_verified_host = None;
1573- }
1574-
1575 /// A greeting must be sent at the start of an SMTP connection when it is
1576 /// first initialized.
1577 /// FIXME
1578 @@ -400,17 +522,9 @@ impl Session {
1579 )
1580 }
1581
1582- /// Check if the capability is supported by the session
1583- pub fn has_capability(&self, capability: u32) -> bool {
1584- self.initialized
1585- .as_ref()
1586- .is_some_and(|mode| matches!(mode, Mode::Extended))
1587- && self.capabilities & capability != 0
1588- }
1589-
1590- /// Ensure that the session has been initialized otherwise return an error
1591- fn check_initialized(&self) -> Result<(), Response<String>> {
1592- if self.initialized.is_none() {
1593+ // /// Ensure that the session has been initialized otherwise return an error
1594+ fn check_initialized(&self, inner: &Inner) -> Result<(), Response<String>> {
1595+ if inner.initialized.is_none() {
1596 return Err(smtp_response!(
1597 500,
1598 5,
1599 @@ -422,54 +536,12 @@ impl Session {
1600 Ok(())
1601 }
1602
1603- /// checks if 8BITMIME is supported
1604- fn check_body(&self, body: &[u8]) -> Result<(), Response<String>> {
1605- if !self.has_capability(smtp_proto::EXT_8BIT_MIME) && !body.is_ascii() {
1606- return Err(smtp_response!(
1607- 500,
1608- 0,
1609- 0,
1610- 0,
1611- "Non ASCII characters found in message body"
1612- ));
1613- }
1614- Ok(())
1615- }
1616-
1617- /// Called each time a message is ready for processing, will do spf
1618- /// validation if it is configured.
1619- fn accept_payload(&mut self, payload: Bytes) -> Action<'_> {
1620- if self.rcpt_to.is_none() {
1621- return Action::Send(smtp_response!(500, 0, 0, 0, "RCPT TO is missing"));
1622- }
1623- if self.hostname.is_none() {
1624- return Action::Send(smtp_response!(500, 0, 0, 0, "Hostname is missing"));
1625- }
1626- let copied = payload.to_vec();
1627- if let Err(response) = self.check_body(&copied) {
1628- return Action::Send(response);
1629- };
1630- let parser = MessageParser::new();
1631- match parser.parse(&copied) {
1632- Some(message) => Action::Envelope {
1633- initial_response: smtp_response!(250, 0, 0, 0, "OK"),
1634- envelope: Envelope {
1635- body: message.into_owned(),
1636- // FIXME
1637- mail_from: self.mail_from.clone().unwrap(),
1638- rcpt_to: self.rcpt_to.clone().unwrap(),
1639- hostname: self.hostname.clone().unwrap(),
1640- },
1641- },
1642- None => Action::Send(smtp_response!(500, 0, 0, 0, "Cannot parse message payload")),
1643- }
1644- }
1645-
1646 /// Process the SMTP command returning the action sometimes with a callback
1647 /// that the implementor needs to take.
1648- pub fn next(&mut self, req: Option<&Request<String>>) -> Action<'_> {
1649+ pub fn next(&mut self, req: Option<&Request<String>>) -> Action {
1650+ let mut inner = self.inner.lock().unwrap();
1651 if let Some(req) = req {
1652- self.history.push(req.clone());
1653+ inner.history.push(req.clone());
1654 }
1655 match req {
1656 None => {
1657 @@ -489,12 +561,12 @@ impl Session {
1658 Some(Request::Ehlo { host }) => {
1659 match Host::parse(&parse_host(host)) {
1660 Ok(hostname) => {
1661- self.hostname = Some(hostname);
1662+ inner.hostname = Some(hostname);
1663 }
1664 Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
1665 };
1666- self.reset();
1667- self.initialized = Some(Mode::Extended);
1668+ inner.reset();
1669+ inner.initialized = Some(Mode::Extended);
1670 let mut resp = EhloResponse::new(format!("Hello {}", host));
1671 resp.capabilities = self.capabilities;
1672 resp.size = self.maximum_size as usize;
1673 @@ -506,27 +578,27 @@ impl Session {
1674 Some(Request::Lhlo { host }) => {
1675 match Host::parse(&parse_host(host)) {
1676 Ok(hostname) => {
1677- self.hostname = Some(hostname);
1678+ inner.hostname = Some(hostname);
1679 }
1680 Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
1681 };
1682- self.reset();
1683- self.initialized = Some(Mode::Legacy);
1684+ inner.reset();
1685+ inner.initialized = Some(Mode::Legacy);
1686 Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
1687 }
1688 Some(Request::Helo { host }) => {
1689 match Host::parse(&parse_host(host)) {
1690 Ok(hostname) => {
1691- self.hostname = Some(hostname);
1692+ inner.hostname = Some(hostname);
1693 }
1694 Err(e) => return Action::Send(smtp_response!(500, 0, 0, 0, e.to_string())),
1695 };
1696- self.reset();
1697- self.initialized = Some(Mode::Legacy);
1698+ inner.reset();
1699+ inner.initialized = Some(Mode::Legacy);
1700 Action::Send(smtp_response!(250, 0, 0, 0, format!("Hello {}", host)))
1701 }
1702 Some(Request::Mail { from }) => {
1703- if let Some(err) = self.check_initialized().err() {
1704+ if let Some(err) = self.check_initialized(&inner).err() {
1705 return Action::Send(err);
1706 }
1707 let mail_from = match EmailAddress::from_str(&from.address) {
1708 @@ -541,11 +613,11 @@ impl Session {
1709 ))
1710 }
1711 };
1712- self.mail_from = Some(mail_from.clone());
1713+ inner.mail_from = Some(mail_from.clone());
1714 Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
1715 }
1716 Some(Request::Rcpt { to }) => {
1717- if let Some(err) = self.check_initialized().err() {
1718+ if let Some(err) = self.check_initialized(&inner).err() {
1719 return Action::Send(err);
1720 }
1721 let rcpt_to = match EmailAddress::from_str(to.address.as_str()) {
1722 @@ -560,10 +632,10 @@ impl Session {
1723 ))
1724 }
1725 };
1726- if let Some(ref mut rcpts) = self.rcpt_to {
1727+ if let Some(ref mut rcpts) = inner.rcpt_to {
1728 rcpts.push(rcpt_to.clone());
1729 } else {
1730- self.rcpt_to = Some(vec![rcpt_to.clone()]);
1731+ inner.rcpt_to = Some(vec![rcpt_to.clone()]);
1732 }
1733 Action::Send(smtp_response!(250, 0, 0, 0, "OK"))
1734 }
1735 @@ -571,10 +643,9 @@ impl Session {
1736 chunk_size: _,
1737 is_last: _,
1738 }) => {
1739- if let Some(err) = self.check_initialized().err() {
1740+ if let Some(err) = self.check_initialized(&inner).err() {
1741 return Action::Send(err);
1742 }
1743- let inner = self;
1744 tracing::info!("Starting binary data transfer");
1745 Action::Message {
1746 initial_response: smtp_response!(
1747 @@ -584,7 +655,13 @@ impl Session {
1748 0,
1749 "Starting BDAT data transfer".to_string()
1750 ),
1751- cb: Box::new(move |payload| inner.accept_payload(payload.to_vec().into())),
1752+ cb: SetMessage {
1753+ inner: self.inner.clone(),
1754+ capabilities: Capabilities {
1755+ mode: inner.initialized.clone(),
1756+ capabilities: self.capabilities,
1757+ },
1758+ },
1759 }
1760 }
1761 // After an AUTH command has been successfully completed, no more
1762 @@ -595,7 +672,7 @@ impl Session {
1763 mechanism,
1764 initial_response,
1765 }) => {
1766- if let Some(err) = self.check_initialized().err() {
1767+ if let Some(err) = self.check_initialized(&inner).err() {
1768 return Action::Send(err);
1769 }
1770 if self.flags.authentication {
1771 @@ -607,34 +684,24 @@ impl Session {
1772 Ok(auth_data) => auth_data,
1773 Err(e) => return Action::Send(e.into()),
1774 };
1775- // TODO: Let the auth callback return this instead
1776- let authcid = auth_data.authcid().clone();
1777- let inner = self;
1778 Action::PlainAuth {
1779 authcid: auth_data.authcid(),
1780 authzid: auth_data.authzid(),
1781 password: auth_data.passwd(),
1782- cb: Box::new(move |result| match result {
1783- Ok(_) => {
1784- tracing::info!("Successfully authenticated");
1785- inner.authenticated_id = Some(authcid);
1786- Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
1787- }
1788- Err(e) => Action::Send(e.into()),
1789- }),
1790+ cb: PlainAuth(self.inner.clone()),
1791 }
1792 } else {
1793 Action::Send(smtp_response!(504, 5, 5, 4, "Auth Not Supported"))
1794 }
1795 }
1796 Some(Request::Noop { value: _ }) => {
1797- if let Some(err) = self.check_initialized().err() {
1798+ if let Some(err) = self.check_initialized(&inner).err() {
1799 return Action::Send(err);
1800 }
1801 Action::Send(smtp_response!(250, 0, 0, 0, "OK".to_string()))
1802 }
1803 Some(Request::Vrfy { value }) => {
1804- if let Some(err) = self.check_initialized().err() {
1805+ if let Some(err) = self.check_initialized(&inner).err() {
1806 return Action::Send(err);
1807 }
1808 if self.flags.vrfy {
1809 @@ -652,41 +719,27 @@ impl Session {
1810 };
1811 Action::Verify {
1812 address,
1813- cb: Box::new(move |result| match result {
1814- Ok(_) => Action::Send(smtp_response!(200, 0, 0, 0, "OK")),
1815- Err(e) => Action::Send(e.into()),
1816- }),
1817+ cb: Verify,
1818 }
1819 } else {
1820 Action::Send(smtp_response!(500, 0, 0, 0, "VRFY Unavailable"))
1821 }
1822 }
1823 Some(Request::Expn { value }) => {
1824- if let Some(err) = self.check_initialized().err() {
1825+ if let Some(err) = self.check_initialized(&inner).err() {
1826 return Action::Send(err);
1827 }
1828- if self.flags.expn && self.authenticated_id.is_some() {
1829+ if self.flags.expn && inner.authenticated_id.is_some() {
1830 Action::Expand {
1831 address: value.clone(),
1832- cb: Box::new(move |result| match result {
1833- Ok(addresses) => {
1834- let mut responses = vec![smtp_response!(250, 0, 0, 0, "OK")];
1835- responses.extend(
1836- addresses
1837- .iter()
1838- .map(|addr| smtp_response!(250, 0, 0, 0, addr.to_string())),
1839- );
1840- Action::SendMany(responses)
1841- }
1842- Err(e) => Action::Send(e.into()),
1843- }),
1844+ cb: Expand,
1845 }
1846 } else {
1847 Action::Send(smtp_response!(500, 0, 0, 0, "EXPN Unavailable"))
1848 }
1849 }
1850 Some(Request::Help { value }) => {
1851- if let Some(err) = self.check_initialized().err() {
1852+ if let Some(err) = self.check_initialized(&inner).err() {
1853 return Action::Send(err);
1854 }
1855 if value.is_empty() {
1856 @@ -726,11 +779,10 @@ impl Session {
1857 }
1858 }
1859 Some(Request::Data) => {
1860- if let Some(err) = self.check_initialized().err() {
1861+ if let Some(err) = self.check_initialized(&inner).err() {
1862 return Action::Send(err);
1863 }
1864 tracing::info!("Starting data transfer");
1865- let inner = self;
1866 Action::Message {
1867 initial_response: smtp_response!(
1868 354,
1869 @@ -739,14 +791,20 @@ impl Session {
1870 0,
1871 "Reading data input, end the message with <CRLF>.<CRLF>".to_string()
1872 ),
1873- cb: Box::new(move |payload| inner.accept_payload(payload.to_vec().into())),
1874+ cb: SetMessage {
1875+ inner: self.inner.clone(),
1876+ capabilities: Capabilities {
1877+ mode: inner.initialized.clone(),
1878+ capabilities: self.capabilities,
1879+ },
1880+ },
1881 }
1882 }
1883 Some(Request::Rset) => {
1884- if let Some(err) = self.check_initialized().err() {
1885+ if let Some(err) = self.check_initialized(&inner).err() {
1886 return Action::Send(err);
1887 }
1888- self.reset();
1889+ inner.reset();
1890 Action::Send(smtp_response!(200, 0, 0, 0, "".to_string()))
1891 }
1892 Some(Request::Quit) => Action::Quit(smtp_response!(221, 0, 0, 0, "Ciao!".to_string())),
1893 @@ -765,7 +823,7 @@ mod test {
1894
1895 const EXAMPLE_HOSTNAME: &str = "example.org";
1896
1897- fn equal(actual: &Action<'_>, expected: &Action<'_>) -> bool {
1898+ fn equal(actual: &Action, expected: &Action) -> bool {
1899 let is_equal = match actual {
1900 Action::Send(response) => {
1901 matches!(expected, Action::Send(other) if response.eq(other))
1902 @@ -849,6 +907,9 @@ mod test {
1903 ));
1904
1905 assert!(session
1906+ .inner
1907+ .lock()
1908+ .unwrap()
1909 .hostname
1910 .as_ref()
1911 .is_some_and(|hostname| hostname.to_string() == EXAMPLE_HOSTNAME));
1912 @@ -906,7 +967,7 @@ mod test {
1913 assert!(authzid == "hello");
1914 assert!(password == "world");
1915 assert!(equal(
1916- &cb(Ok(())),
1917+ &cb.call(Ok(authcid.clone())),
1918 &Action::Send(smtp_response!(235, 2, 7, 0, "OK"))
1919 ));
1920 }
1921 @@ -915,6 +976,9 @@ mod test {
1922 };
1923
1924 assert!(session
1925+ .inner
1926+ .lock()
1927+ .unwrap()
1928 .authenticated_id
1929 .as_ref()
1930 .is_some_and(|id| id == "hello"));
1931 @@ -923,14 +987,17 @@ mod test {
1932 #[test]
1933 fn session_expand() {
1934 let session = &mut Session::default().authentication(true).expn_enabled(true);
1935- session.initialized = Some(Mode::Extended);
1936- session.authenticated_id = Some("hello".to_string());
1937+ session.inner = Arc::new(Mutex::new(Inner {
1938+ initialized: Some(Mode::Extended),
1939+ authenticated_id: Some("hello".to_string()),
1940+ ..Default::default()
1941+ }));
1942 match session.next(Some(&Request::Expn {
1943 value: String::from("group@baz.com"),
1944 })) {
1945 Action::Expand { address: _, cb } => {
1946 assert!(equal(
1947- &cb(Ok(vec![
1948+ &cb.call(Ok(vec![
1949 EmailAddress::new_unchecked("fuu@bar.com"),
1950 EmailAddress::new_unchecked("baz@qux.com")
1951 ])),
1952 @@ -948,15 +1015,18 @@ mod test {
1953 #[test]
1954 fn session_verify() {
1955 let session = &mut Session::default().authentication(true).vrfy_enabled(true);
1956- session.initialized = Some(Mode::Extended);
1957- session.authenticated_id = Some("hello".to_string());
1958+ session.inner = Arc::new(Mutex::new(Inner {
1959+ initialized: Some(Mode::Extended),
1960+ authenticated_id: Some("hello".to_string()),
1961+ ..Default::default()
1962+ }));
1963 match session.next(Some(&Request::Vrfy {
1964 value: String::from("qux@baz.com"),
1965 })) {
1966 Action::Verify { address, cb } => {
1967 assert!(address.to_string() == "qux@baz.com");
1968 assert!(equal(
1969- &cb(Ok(())),
1970+ &cb.call(Ok(())),
1971 &Action::Send(smtp_response!(200, 0, 0, 0, "OK"))
1972 ));
1973 }
1974 @@ -968,10 +1038,14 @@ mod test {
1975 fn session_non_ascii_characters_legacy_smtp() {
1976 let session = &mut Session::default();
1977 // non-extended sessions cannot accept non-ascii characters
1978- session.initialized = Some(Mode::Legacy);
1979- session.hostname = Some(Host::Domain(String::from("bar.com")));
1980- session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com"));
1981- session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]);
1982+ session.inner = Arc::new(Mutex::new(Inner {
1983+ initialized: Some(Mode::Legacy),
1984+ hostname: Some(Host::parse("example.org").unwrap()),
1985+ authenticated_id: Some("hello".to_string()),
1986+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
1987+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
1988+ ..Default::default()
1989+ }));
1990 match session.next(Some(&Request::Data {})) {
1991 Action::Message {
1992 initial_response,
1993 @@ -987,13 +1061,13 @@ mod test {
1994 "Reading data input, end the message with <CRLF>.<CRLF>"
1995 ))
1996 ));
1997- let action = cb(Bytes::from_static(
1998+ let action = cb.call(
1999 r#"
2000 Subject: Hello World
2001 😍😍😍
2002 "#
2003 .as_bytes(),
2004- ));
2005+ );
2006 assert!(equal(
2007 &action,
2008 &Action::Send(smtp_response!(
2009 @@ -1013,10 +1087,14 @@ Subject: Hello World
2010 fn session_non_ascii_characters_extended_smtp() {
2011 let session = &mut Session::default();
2012 // non-extended sessions cannot accept non-ascii characters
2013- session.initialized = Some(Mode::Extended);
2014- session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com"));
2015- session.hostname = Some(Host::Domain(String::from("bar.com")));
2016- session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]);
2017+ session.inner = Arc::new(Mutex::new(Inner {
2018+ initialized: Some(Mode::Extended),
2019+ hostname: Some(Host::parse("example.org").unwrap()),
2020+ authenticated_id: Some("hello".to_string()),
2021+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
2022+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
2023+ ..Default::default()
2024+ }));
2025 match session.next(Some(&Request::Data {})) {
2026 Action::Message {
2027 initial_response,
2028 @@ -1032,13 +1110,13 @@ Subject: Hello World
2029 "Reading data input, end the message with <CRLF>.<CRLF>"
2030 ))
2031 ));
2032- let action = cb(Bytes::from_static(
2033+ let action = cb.call(
2034 r#"
2035 Subject: Hello World
2036 😍😍😍
2037 "#
2038 .as_bytes(),
2039- ));
2040+ );
2041 assert!(equal(
2042 &action,
2043 &Action::Envelope {
2044 @@ -1060,10 +1138,14 @@ Subject: Hello World
2045 fn session_message_body_ok() {
2046 let session = &mut Session::default();
2047 // non-extended sessions cannot accept non-ascii characters
2048- session.initialized = Some(Mode::Extended);
2049- session.hostname = Some(Host::Domain(String::from("bar.com")));
2050- session.rcpt_to = Some(vec![EmailAddress::new_unchecked("qux@baz.com")]);
2051- session.mail_from = Some(EmailAddress::new_unchecked("fuu@bar.com"));
2052+ session.inner = Arc::new(Mutex::new(Inner {
2053+ hostname: Some(Host::parse("example.org").unwrap()),
2054+ initialized: Some(Mode::Extended),
2055+ authenticated_id: Some("hello".to_string()),
2056+ mail_from: Some(EmailAddress::new_unchecked("fuu@bar.com")),
2057+ rcpt_to: Some(vec![EmailAddress::new_unchecked("qux@baz.com")]),
2058+ ..Default::default()
2059+ }));
2060 {
2061 match session.next(Some(&Request::Data {})) {
2062 Action::Message {
2063 @@ -1080,7 +1162,7 @@ Subject: Hello World
2064 "Reading data input, end the message with <CRLF>.<CRLF>"
2065 ))
2066 ));
2067- let action = cb(Bytes::from_static(
2068+ let action = cb.call(
2069 r#"To: <baz@qux.com>
2070 Subject: Hello World
2071
2072 @@ -1090,7 +1172,7 @@ Note that it doesn't end with a "." since that parsing happens as part of the
2073 transport rather than the session. 🩷
2074 "#
2075 .as_bytes(),
2076- ));
2077+ );
2078 assert!(equal(
2079 &action,
2080 &Action::Envelope {
2081 diff --git a/maitred/src/verify.rs b/maitred/src/verify.rs
2082index 6497908..7ade63b 100644
2083--- a/maitred/src/verify.rs
2084+++ b/maitred/src/verify.rs
2085 @@ -42,7 +42,7 @@ impl Into<Response<String>> for VerifyError {
2086 /// Verify that the given e-mail address exists on the server. Servers may
2087 /// choose to implement nothing or not use this option at all if desired.
2088 #[async_trait]
2089- pub trait Verify {
2090+ pub trait Verify: Sync + Send {
2091 /// Verify the e-mail address on the server
2092 async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError>;
2093 }
2094 @@ -62,13 +62,13 @@ pub trait Verify {
2095 /// ```
2096 pub struct VerifyFunc<F, T>(pub F)
2097 where
2098- F: Fn(&EmailAddress) -> T + Sync,
2099+ F: Fn(&EmailAddress) -> T + Sync + Send,
2100 T: Future<Output = Result<(), VerifyError>> + Send;
2101
2102 #[async_trait]
2103 impl<F, T> Verify for VerifyFunc<F, T>
2104 where
2105- F: Fn(&EmailAddress) -> T + Sync,
2106+ F: Fn(&EmailAddress) -> T + Sync + Send,
2107 T: Future<Output = Result<(), VerifyError>> + Send,
2108 {
2109 async fn verify(&self, address: &EmailAddress) -> Result<(), VerifyError> {