Compare commits
1060 Commits
v0.5.1
...
better-tls
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
716d7baac2 | ||
|
|
659b0b50b1 | ||
|
|
f332439000 | ||
|
|
335cdea3f9 | ||
|
|
d3d8e24824 | ||
|
|
c4df9730aa | ||
|
|
bfed19e6ad | ||
|
|
629967ac98 | ||
|
|
06e381ec9c | ||
|
|
d9ce9a6e47 | ||
|
|
e892b55b6b | ||
|
|
7642b2130e | ||
|
|
5a3f189e50 | ||
|
|
31b8f297ec | ||
|
|
3a6ab6f398 | ||
|
|
7b8fc5a678 | ||
|
|
2b36935b1f | ||
|
|
d31490a2a9 | ||
|
|
b7482f0232 | ||
|
|
abc8cdf789 | ||
|
|
81b233def4 | ||
|
|
e644a6c2d3 | ||
|
|
0385ca3b19 | ||
|
|
d114f9caf3 | ||
|
|
785307b091 | ||
|
|
f16cbeec51 | ||
|
|
5cbe9ba283 | ||
|
|
2f4e36ac61 | ||
|
|
610b72e93b | ||
|
|
69b7c5500a | ||
|
|
63c5fcccfc | ||
|
|
b583aff36c | ||
|
|
512c5e3ce8 | ||
|
|
771d212198 | ||
|
|
83ba93944d | ||
|
|
de3ab006e2 | ||
|
|
9504b7f45c | ||
|
|
c91b356a96 | ||
|
|
118c1ad47f | ||
|
|
8bf4d3a9c1 | ||
|
|
1fcff673ba | ||
|
|
8c70c0cfb4 | ||
|
|
63d8d30088 | ||
|
|
6c0be84817 | ||
|
|
6059cb04d6 | ||
|
|
fdf0346556 | ||
|
|
0f9455715c | ||
|
|
0b3a1ed278 | ||
|
|
76bf68268f | ||
|
|
99a86c0fac | ||
|
|
f0de9ef02c | ||
|
|
b4ddcbdcfc | ||
|
|
1e22bcd527 | ||
|
|
75716ca269 | ||
|
|
8a6f1dab0e | ||
|
|
621853e2e3 | ||
|
|
5e4cb2d1b5 | ||
|
|
b4abd40698 | ||
|
|
2d1ccda2ef | ||
|
|
54934e1492 | ||
|
|
cfa29743a8 | ||
|
|
4a4a96d805 | ||
|
|
f0b8052a52 | ||
|
|
655cd8a140 | ||
|
|
dabc88a053 | ||
|
|
9cdefcea09 | ||
|
|
5748af4c98 | ||
|
|
3e9b1876d9 | ||
|
|
795bedae76 | ||
|
|
891dd521ab | ||
|
|
0fb89e23ad | ||
|
|
097f7d5aaa | ||
|
|
32e066464a | ||
|
|
55c7f57f25 | ||
|
|
3f7a57a417 | ||
|
|
bb64baec67 | ||
|
|
5f13636b49 | ||
|
|
4513e602d6 | ||
|
|
382e15013a | ||
|
|
3ce31c5a6a | ||
|
|
a48cf92a5b | ||
|
|
43f6f139d2 | ||
|
|
fd1425666d | ||
|
|
de075153b0 | ||
|
|
02dfc7dd4a | ||
|
|
83ce5872d7 | ||
|
|
272efeca74 | ||
|
|
ec6f5f3920 | ||
|
|
b62d23bd87 | ||
|
|
51794aa912 | ||
|
|
eb42651401 | ||
|
|
99c6dc2a87 | ||
|
|
b6babbce00 | ||
|
|
c9895c52de | ||
|
|
575492b9ed | ||
|
|
ad665cd01e | ||
|
|
e2ac5dadfb | ||
|
|
1c6a348eb8 | ||
|
|
e8b2498ad7 | ||
|
|
bf48bd6b96 | ||
|
|
fa6191983a | ||
|
|
ca405040ae | ||
|
|
f7a1b790df | ||
|
|
caff354cbf | ||
|
|
a81401c4cb | ||
|
|
54df594d6c | ||
|
|
cada01d039 | ||
|
|
0132bee59d | ||
|
|
acdf189717 | ||
|
|
3aea65315f | ||
|
|
9d3ebfab1a | ||
|
|
6fb69086fb | ||
|
|
dfdf3a61d2 | ||
|
|
e30ac2dbff | ||
|
|
22dca340a7 | ||
|
|
c7d1f35676 | ||
|
|
eebea56f16 | ||
|
|
851d6ae164 | ||
|
|
6f38e6b9a9 | ||
|
|
c40af78809 | ||
|
|
6d2e0d5046 | ||
|
|
c64cb0ff2e | ||
|
|
10d7b197ed | ||
|
|
fb54855d5f | ||
|
|
157c4fb5ae | ||
|
|
1196e332ee | ||
|
|
75770f7bc6 | ||
|
|
76d0929c94 | ||
|
|
c3d00051b2 | ||
|
|
12580d82f4 | ||
|
|
f7849078b8 | ||
|
|
f2c94cdf4d | ||
|
|
74f64b81ab | ||
|
|
39c71dbfd2 | ||
|
|
c1bf5dfda1 | ||
|
|
1c1fef8055 | ||
|
|
1540f16015 | ||
|
|
330daa1173 | ||
|
|
47f2fe0750 | ||
|
|
8b6cee30ee | ||
|
|
62c16e90ef | ||
|
|
e0494a5f9d | ||
|
|
8c3bffa728 | ||
|
|
47eda90433 | ||
|
|
46ea8c48ac | ||
|
|
5f7063fdc3 | ||
|
|
61c1f6bc6f | ||
|
|
283e21f8d6 | ||
|
|
20c3701eb0 | ||
|
|
74117d5cc6 | ||
|
|
bb49e0a46b | ||
|
|
42365478c2 | ||
|
|
94769242d1 | ||
|
|
7e6ffe8aea | ||
|
|
16c35ef583 | ||
|
|
bbab86b484 | ||
|
|
b5652f18b7 | ||
|
|
c2f2b907a9 | ||
|
|
a1cc770613 | ||
|
|
57886c367d | ||
|
|
f3a469431e | ||
|
|
9b48ef355b | ||
|
|
7fee8dc5a8 | ||
|
|
7e9fff9bd0 | ||
|
|
92f5460132 | ||
|
|
cd0c032f71 | ||
|
|
f41c9c19ab | ||
|
|
cb6a7178d9 | ||
|
|
2bfc759aa3 | ||
|
|
89673d0eb2 | ||
|
|
8b588cf275 | ||
|
|
5f37b66352 | ||
|
|
69e5974024 | ||
|
|
4fb67a7da1 | ||
|
|
9041f210f4 | ||
|
|
77b7d40fb8 | ||
|
|
2b6d457f85 | ||
|
|
952c1b39df | ||
|
|
7ecb87f9fd | ||
|
|
fd700b1717 | ||
|
|
f8f19d6af5 | ||
|
|
cc25223914 | ||
|
|
750573d38b | ||
|
|
0734a96343 | ||
|
|
3c2f996856 | ||
|
|
9cae29dd07 | ||
|
|
e1a146c8f8 | ||
|
|
840a19784a | ||
|
|
5a61ba36b5 | ||
|
|
dbf0e53c31 | ||
|
|
c914a07379 | ||
|
|
2c4fa39523 | ||
|
|
28f0af16be | ||
|
|
f0614be555 | ||
|
|
a3fcdf263d | ||
|
|
d4da2e1f14 | ||
|
|
5655958288 | ||
|
|
11b4acf0cd | ||
|
|
b3b5df285a | ||
|
|
3c051d52e7 | ||
|
|
d6128a146e | ||
|
|
fab6680150 | ||
|
|
0c9fc6cb71 | ||
|
|
2228cbdf93 | ||
|
|
17c95b0fa8 | ||
|
|
62725af00a | ||
|
|
758bf1a4a7 | ||
|
|
054c79f914 | ||
|
|
985fa7edc4 | ||
|
|
9004d4ccc5 | ||
|
|
10171f8c75 | ||
|
|
99e805952d | ||
|
|
2d21dde5a1 | ||
|
|
6fec936c0c | ||
|
|
22dfa5aa96 | ||
|
|
44e4cfd622 | ||
|
|
7ea3d38a00 | ||
|
|
73b89f5a9f | ||
|
|
1ec1b705c9 | ||
|
|
e4006518fe | ||
|
|
b33dd562fc | ||
|
|
65958df14f | ||
|
|
50628af5fd | ||
|
|
cf858cc682 | ||
|
|
f9a4b5ba89 | ||
|
|
1391a834ce | ||
|
|
e6b4529896 | ||
|
|
ca5cb3f8f7 | ||
|
|
1e2279457e | ||
|
|
961364cc29 | ||
|
|
b0db759e5f | ||
|
|
5daf5d397a | ||
|
|
3f1647fa48 | ||
|
|
fd106d9b0c | ||
|
|
c1d37d54b4 | ||
|
|
efa0d58778 | ||
|
|
9567b23f4d | ||
|
|
f77376fa19 | ||
|
|
6e35b9b30d | ||
|
|
c24213c850 | ||
|
|
8b40e438fd | ||
|
|
e1462b2d1b | ||
|
|
96b42515cd | ||
|
|
1ea4987023 | ||
|
|
9273d24e54 | ||
|
|
7a0dd5bd92 | ||
|
|
9a8aa46dba | ||
|
|
0377ea29b7 | ||
|
|
89e5b9083e | ||
|
|
8c370e28c9 | ||
|
|
3eed80ef30 | ||
|
|
dbb135c533 | ||
|
|
4c5f02b4f6 | ||
|
|
f02542841c | ||
|
|
29c34adc25 | ||
|
|
5e3ebbb189 | ||
|
|
60399a93cc | ||
|
|
a48bc8a1b2 | ||
|
|
94cc0149d1 | ||
|
|
a89383cdb6 | ||
|
|
592593f4b8 | ||
|
|
97d3c760c0 | ||
|
|
8f28b0c341 | ||
|
|
dc9c5df210 | ||
|
|
c9b3fa0baa | ||
|
|
addf8754dd | ||
|
|
af157c5f26 | ||
|
|
3e8988ae55 | ||
|
|
941a00bcaa | ||
|
|
14079bff8c | ||
|
|
696c06e8d7 | ||
|
|
d4f7618898 | ||
|
|
e0a0a2e624 | ||
|
|
9ab6bb56d3 | ||
|
|
e1d3778329 | ||
|
|
623d69c553 | ||
|
|
55c2618201 | ||
|
|
9f550bce86 | ||
|
|
e875d9ff64 | ||
|
|
aadcc0f83c | ||
|
|
b534a18017 | ||
|
|
0684bccd47 | ||
|
|
4471759221 | ||
|
|
ed454819ee | ||
|
|
47cad567b0 | ||
|
|
b0e2fc9bca | ||
|
|
1d8249165c | ||
|
|
98fc0cb2f3 | ||
|
|
0439bab874 | ||
|
|
504fc51b26 | ||
|
|
d54343cf00 | ||
|
|
904789ac3d | ||
|
|
94cae6df0d | ||
|
|
f17dccc46d | ||
|
|
7e7f05eb45 | ||
|
|
99df9e8d7c | ||
|
|
1b5109b6ac | ||
|
|
a4be3c4cd8 | ||
|
|
4586f2ad8a | ||
|
|
31de9e508b | ||
|
|
69334fe5eb | ||
|
|
2ad2444183 | ||
|
|
8afa442e93 | ||
|
|
486e0f9d50 | ||
|
|
acc4ff4898 | ||
|
|
1728d57c34 | ||
|
|
53bfb65423 | ||
|
|
61b08814c9 | ||
|
|
0e74042b4e | ||
|
|
29affe9398 | ||
|
|
b10f6ff8de | ||
|
|
2002a9d75a | ||
|
|
1193e1134d | ||
|
|
7c6ade7afe | ||
|
|
3bc729ca64 | ||
|
|
f041c00df7 | ||
|
|
fe8dc4967d | ||
|
|
137566a4e4 | ||
|
|
216c612931 | ||
|
|
a429a24913 | ||
|
|
648bf2b2f6 | ||
|
|
509a623a27 | ||
|
|
a681c6b49d | ||
|
|
22efe341fe | ||
|
|
97fba6a47e | ||
|
|
f7066ac858 | ||
|
|
9379f2e328 | ||
|
|
05133a7102 | ||
|
|
d7d05bf48a | ||
|
|
34ac265d60 | ||
|
|
bbf56de83d | ||
|
|
b594945695 | ||
|
|
5c83120986 | ||
|
|
d4df9a2965 | ||
|
|
d2aa959845 | ||
|
|
d1f016e8e2 | ||
|
|
9146212a3e | ||
|
|
a04866acfb | ||
|
|
be88aabae2 | ||
|
|
6fbb3bf440 | ||
|
|
9d8c31bef8 | ||
|
|
0ea3bfbd13 | ||
|
|
a0980d017b | ||
|
|
40c8a9d000 | ||
|
|
ed50ea74ba | ||
|
|
20d0f8f3ba | ||
|
|
690b143ea3 | ||
|
|
7f384bc983 | ||
|
|
d8c4a66206 | ||
|
|
54cd221de7 | ||
|
|
1a0c344c91 | ||
|
|
15030fde53 | ||
|
|
89fa5cdb80 | ||
|
|
aac5c9929f | ||
|
|
210133a078 | ||
|
|
a4c0af9cf1 | ||
|
|
ad9699827e | ||
|
|
f06b8f3823 | ||
|
|
170e929a2b | ||
|
|
d3c73d8bd7 | ||
|
|
4e13963bf3 | ||
|
|
ca6399acbf | ||
|
|
3f03b6296b | ||
|
|
3683d122ba | ||
|
|
0e3526c1bc | ||
|
|
50ac1cdbec | ||
|
|
227da8ac09 | ||
|
|
86763ccefb | ||
|
|
65d952a64f | ||
|
|
2e8d43baae | ||
|
|
648cf4ce5e | ||
|
|
4a9d4fbf7e | ||
|
|
a0da9fc2b9 | ||
|
|
aa31e4fff6 | ||
|
|
b187885e70 | ||
|
|
6216fd92c8 | ||
|
|
0de49c000c | ||
|
|
b55bcfb6fb | ||
|
|
04f064ed5a | ||
|
|
13b48b656d | ||
|
|
2ac6a72a8e | ||
|
|
f1e86c809d | ||
|
|
0b8d5d20ad | ||
|
|
313eb74ea5 | ||
|
|
6afc078545 | ||
|
|
697da9f7db | ||
|
|
1ea562b15a | ||
|
|
b43c69af47 | ||
|
|
449f317246 | ||
|
|
17d644181a | ||
|
|
583df6af18 | ||
|
|
ed9ca92de8 | ||
|
|
0174a29a45 | ||
|
|
bfd3300df3 | ||
|
|
6526eff5b2 | ||
|
|
e00eff8b2a | ||
|
|
5beef57c18 | ||
|
|
b10b04ada8 | ||
|
|
30a8797acf | ||
|
|
c5fef28ac9 | ||
|
|
bf32554e51 | ||
|
|
9983bb53c3 | ||
|
|
8e49c60ff8 | ||
|
|
e156520feb | ||
|
|
ec7d63c8de | ||
|
|
e5460c4ba1 | ||
|
|
d2912a3e3f | ||
|
|
9b3bd00a61 | ||
|
|
990de687aa | ||
|
|
31a3be1cba | ||
|
|
47dfdf7ee8 | ||
|
|
47c4077b14 | ||
|
|
8869c7fdb4 | ||
|
|
3cf89935af | ||
|
|
ce957ee346 | ||
|
|
f87c80e05c | ||
|
|
1c4a3f0fb3 | ||
|
|
393d414700 | ||
|
|
a83f927109 | ||
|
|
c59f67d808 | ||
|
|
087e1e9c31 | ||
|
|
69d48c4be7 | ||
|
|
04b42879b0 | ||
|
|
d5c1ab8dd1 | ||
|
|
42a34175ac | ||
|
|
542ea4ffd2 | ||
|
|
41d68616e0 | ||
|
|
36aab20086 | ||
|
|
98f09117f7 | ||
|
|
c0ef9a38a1 | ||
|
|
6b6f130070 | ||
|
|
694a6d2852 | ||
|
|
f865fc1bce | ||
|
|
60e3a0b7cb | ||
|
|
c8ec8984b8 | ||
|
|
1b45c6dd58 | ||
|
|
470b8c3ca7 | ||
|
|
c8d73dd940 | ||
|
|
bcbdbecd95 | ||
|
|
d75fb5956b | ||
|
|
211aa389d7 | ||
|
|
49787e0c41 | ||
|
|
3e62efb46a | ||
|
|
6c440bda73 | ||
|
|
cedfd8bfbb | ||
|
|
889ef0ba6a | ||
|
|
e7f07c5ce8 | ||
|
|
4b238829c7 | ||
|
|
fbbd015109 | ||
|
|
8fa66c1e0f | ||
|
|
72015da467 | ||
|
|
4b693e2ae3 | ||
|
|
ff2baacc3d | ||
|
|
2173bc5f43 | ||
|
|
df6169bc98 | ||
|
|
598abcc589 | ||
|
|
213fe1dc4e | ||
|
|
75e309731e | ||
|
|
95bc3e6745 | ||
|
|
e2b641ae89 | ||
|
|
b3ad137691 | ||
|
|
f86c544792 | ||
|
|
f8ea0c384d | ||
|
|
9e24786f67 | ||
|
|
8a5dc32578 | ||
|
|
f4a580bb90 | ||
|
|
cbef830df9 | ||
|
|
fba900daa5 | ||
|
|
f39f0d1527 | ||
|
|
c41948ccd8 | ||
|
|
43adb0fb11 | ||
|
|
7765a97e7d | ||
|
|
03cbed9b05 | ||
|
|
427fb4e35c | ||
|
|
e87f9950af | ||
|
|
8c8aa770bf | ||
|
|
0d063873fc | ||
|
|
ce08d9e8aa | ||
|
|
83a0310c8c | ||
|
|
c43e205212 | ||
|
|
33b0a9e27d | ||
|
|
4f0ea6366c | ||
|
|
2ade0b8846 | ||
|
|
6499bfe3d4 | ||
|
|
4c7086968f | ||
|
|
6afdb0cf3a | ||
|
|
ed9f38d2c8 | ||
|
|
88df2a502d | ||
|
|
7dd392401c | ||
|
|
8425f1d7c4 | ||
|
|
70c33882cc | ||
|
|
349b349518 | ||
|
|
5acfa398f6 | ||
|
|
e609c6bb72 | ||
|
|
0b98ccad45 | ||
|
|
c352efcb86 | ||
|
|
e640b6fe19 | ||
|
|
4c2f40dcb6 | ||
|
|
18a89d4407 | ||
|
|
4115652695 | ||
|
|
b3414bd1ff | ||
|
|
0604030b91 | ||
|
|
dfbe6e9ba2 | ||
|
|
123794a00d | ||
|
|
1404cc8010 | ||
|
|
a661de16c9 | ||
|
|
0ac3438d32 | ||
|
|
7f22a98f2f | ||
|
|
4e500ded50 | ||
|
|
edc22e842f | ||
|
|
8b1399261f | ||
|
|
de277a2ee2 | ||
|
|
7d29d778ac | ||
|
|
8f31fb9804 | ||
|
|
482d74f7bc | ||
|
|
2fdafcd573 | ||
|
|
53aa5b4df6 | ||
|
|
a440ae5c79 | ||
|
|
16ac97e9d0 | ||
|
|
245c600c82 | ||
|
|
3995ea2983 | ||
|
|
8ed030a476 | ||
|
|
d1b54dc990 | ||
|
|
22eced1dbf | ||
|
|
f0fd4556b5 | ||
|
|
715c169167 | ||
|
|
c4d4413242 | ||
|
|
5667da9174 | ||
|
|
174791532d | ||
|
|
99df787e24 | ||
|
|
ce37464050 | ||
|
|
5e521b0c82 | ||
|
|
55ceb8f85d | ||
|
|
a4a3f33180 | ||
|
|
b20e7a0964 | ||
|
|
c4d91177dc | ||
|
|
f67d32ab86 | ||
|
|
21ae091f42 | ||
|
|
39a06862d4 | ||
|
|
6014f5c3f4 | ||
|
|
947af0acdd | ||
|
|
a90b548b4f | ||
|
|
b379adec28 | ||
|
|
29e4829f69 | ||
|
|
d2675fab82 | ||
|
|
aac3e00f8d | ||
|
|
0a450e64a8 | ||
|
|
536b4451d7 | ||
|
|
0f3f27fdb6 | ||
|
|
eb026838e3 | ||
|
|
14fac980ed | ||
|
|
ff6a4ff910 | ||
|
|
9b432aff7a | ||
|
|
92ee714f9a | ||
|
|
ec184ca5ee | ||
|
|
f306c14575 | ||
|
|
4dc4dd29c5 | ||
|
|
61b4087a40 | ||
|
|
2200cf407d | ||
|
|
afc11951a6 | ||
|
|
4b6ea72aac | ||
|
|
6deeb02139 | ||
|
|
97f60f111e | ||
|
|
4601e0f8c8 | ||
|
|
1c879718cf | ||
|
|
da7b701e60 | ||
|
|
fe79b27b44 | ||
|
|
bc60857ce4 | ||
|
|
e0910ad351 | ||
|
|
ff6408f099 | ||
|
|
7ba7560fbb | ||
|
|
32e2a551b0 | ||
|
|
bdd2076eec | ||
|
|
3eef024f77 | ||
|
|
d227cd4384 | ||
|
|
a4627f139a | ||
|
|
9b825de617 | ||
|
|
05735deb56 | ||
|
|
e5a1248a55 | ||
|
|
0e05e0e792 | ||
|
|
86e51813ca | ||
|
|
83a0185a83 | ||
|
|
ebfd00b146 | ||
|
|
4fbe7004cc | ||
|
|
da8bf9a040 | ||
|
|
6b6eadf134 | ||
|
|
75c640b4a9 | ||
|
|
24d694db3b | ||
|
|
eda7fc1501 | ||
|
|
5bc1cba2eb | ||
|
|
bf2adcabed | ||
|
|
e927d0b2e5 | ||
|
|
6eff9d3bee | ||
|
|
8336528f09 | ||
|
|
e86e33214f | ||
|
|
089b811bbc | ||
|
|
50d96ad8df | ||
|
|
657f2cd5ad | ||
|
|
e900b59008 | ||
|
|
0313576fe1 | ||
|
|
5f75afe05c | ||
|
|
0ead3cde09 | ||
|
|
4597884d31 | ||
|
|
2ad5e81e60 | ||
|
|
cf8f934c56 | ||
|
|
df949f837e | ||
|
|
574afb0c9b | ||
|
|
e17c1b8754 | ||
|
|
93066e4ca0 | ||
|
|
0a3d51dc25 | ||
|
|
56b718f04d | ||
|
|
4828cf4e92 | ||
|
|
c33de49fbb | ||
|
|
629b4b0501 | ||
|
|
4f470a2c3f | ||
|
|
a0c8fb947c | ||
|
|
c638650d0a | ||
|
|
1cbcbbb11f | ||
|
|
c9bd7ed852 | ||
|
|
334ce235ff | ||
|
|
10d362f509 | ||
|
|
2e7bd5708f | ||
|
|
cfe4ebf8cb | ||
|
|
8f1c9dbec5 | ||
|
|
0c055b50d1 | ||
|
|
139c07ca10 | ||
|
|
5bb7316722 | ||
|
|
54f4cfcdab | ||
|
|
9db66ce8e8 | ||
|
|
d5e9ebc0db | ||
|
|
cabc625009 | ||
|
|
101189882a | ||
|
|
be50862d55 | ||
|
|
2d4320ea45 | ||
|
|
0444e7833b | ||
|
|
6997ab7ce4 | ||
|
|
139eb9e67e | ||
|
|
917d34210d | ||
|
|
ed2730a0a7 | ||
|
|
81d174d7ed | ||
|
|
bdb4f78bd3 | ||
|
|
4a76fbb46c | ||
|
|
52535c4554 | ||
|
|
5918803778 | ||
|
|
72f3cd8f12 | ||
|
|
78ba8007cd | ||
|
|
e202eafb7f | ||
|
|
adbd50a6ce | ||
|
|
058fa694f0 | ||
|
|
f64721702f | ||
|
|
a8d8e2ac00 | ||
|
|
434654e9af | ||
|
|
4a77f587c3 | ||
|
|
c988b1760a | ||
|
|
e08d4e3ee5 | ||
|
|
fc91bb6ee8 | ||
|
|
ee31bbe9e3 | ||
|
|
0b92881b48 | ||
|
|
8bb97e62ca | ||
|
|
8ba1c3a3f7 | ||
|
|
644b1e59b0 | ||
|
|
e225afbec2 | ||
|
|
22555f0620 | ||
|
|
c09e3ff8cd | ||
|
|
c10fe3db84 | ||
|
|
9d14630552 | ||
|
|
186ad29424 | ||
|
|
dce8d53310 | ||
|
|
c9c82495ce | ||
|
|
283d45824e | ||
|
|
0ee089fc37 | ||
|
|
7f545301e1 | ||
|
|
ce932c15d6 | ||
|
|
afc23de20f | ||
|
|
c52c28ab80 | ||
|
|
4f9067f258 | ||
|
|
964b9dc00b | ||
|
|
866c804ef3 | ||
|
|
a7d35325ed | ||
|
|
319be26031 | ||
|
|
3a0b6e1a31 | ||
|
|
ed7c16452c | ||
|
|
2e2f614517 | ||
|
|
bc09aa2185 | ||
|
|
bab4519baa | ||
|
|
1f1359502e | ||
|
|
5423f53bad | ||
|
|
4b48bdbd9a | ||
|
|
31442e96d0 | ||
|
|
70720d7cdd | ||
|
|
706ed8b4fd | ||
|
|
da63de72fc | ||
|
|
d71b560077 | ||
|
|
917ecbc477 | ||
|
|
4a61357205 | ||
|
|
e7e0f3485d | ||
|
|
33d20c61d1 | ||
|
|
1baf8a9516 | ||
|
|
5bb4f4f8e7 | ||
|
|
2e56bd6a82 | ||
|
|
3159981e4a | ||
|
|
a0c95f748e | ||
|
|
f949dd53ed | ||
|
|
d990ab4de3 | ||
|
|
c489a0bdc2 | ||
|
|
9dd08ad4c2 | ||
|
|
4313207896 | ||
|
|
944a236aa7 | ||
|
|
ddd80f5dcd | ||
|
|
530b595424 | ||
|
|
f985cf7559 | ||
|
|
7b6ac2e677 | ||
|
|
6797d3d3f5 | ||
|
|
bffe2978d2 | ||
|
|
4d86840bc9 | ||
|
|
a7e5493aad | ||
|
|
d6828f5150 | ||
|
|
0b850e3b2f | ||
|
|
57be14112a | ||
|
|
928fd413a4 | ||
|
|
2d196599c7 | ||
|
|
ad2fef9bbc | ||
|
|
6577aa17d9 | ||
|
|
34fc101f31 | ||
|
|
91f0cfa27c | ||
|
|
78bd429310 | ||
|
|
54e3dd3e41 | ||
|
|
36381eb345 | ||
|
|
8d92dbb0c2 | ||
|
|
c4b1100bdb | ||
|
|
b5e2c67dbd | ||
|
|
bf3bb78534 | ||
|
|
a914d9990c | ||
|
|
a3d6722e7e | ||
|
|
fd56ec8877 | ||
|
|
81bad13175 | ||
|
|
27c8e206cf | ||
|
|
b4d03ead8c | ||
|
|
d692a9488f | ||
|
|
f3271715ec | ||
|
|
ee51cf7454 | ||
|
|
8981a7758c | ||
|
|
b91cb0770d | ||
|
|
0cff889ace | ||
|
|
d00568cbd6 | ||
|
|
32cace1252 | ||
|
|
a86cc3328e | ||
|
|
b51b2843f4 | ||
|
|
91a17ae281 | ||
|
|
bd752daf85 | ||
|
|
ed01efd890 | ||
|
|
088db45e41 | ||
|
|
955a453df9 | ||
|
|
71eda4b174 | ||
|
|
d283254b1a | ||
|
|
f3f963c6a5 | ||
|
|
bef45c48f7 | ||
|
|
e024806402 | ||
|
|
d7a8574464 | ||
|
|
9b22f5867e | ||
|
|
17abeb3957 | ||
|
|
e6a5c158da | ||
|
|
f4fc427a03 | ||
|
|
662072e692 | ||
|
|
4f16d9ee69 | ||
|
|
9d68629bb6 | ||
|
|
96e4f845ec | ||
|
|
4dc95281ad | ||
|
|
f3311456ad | ||
|
|
98a250f015 | ||
|
|
f2f2f98905 | ||
|
|
53e79d9620 | ||
|
|
4f11ae61ef | ||
|
|
9344ff7e5c | ||
|
|
36d20bc7b6 | ||
|
|
620c3e96dc | ||
|
|
7cab860cde | ||
|
|
f10e4e81d0 | ||
|
|
5face8614b | ||
|
|
480ed11785 | ||
|
|
a082da6ea4 | ||
|
|
2a847c1b3b | ||
|
|
f6c07f0720 | ||
|
|
bff687f55c | ||
|
|
66cd6fe3ac | ||
|
|
bc4714a2c8 | ||
|
|
547be305c5 | ||
|
|
ba719f7255 | ||
|
|
4005fc88bc | ||
|
|
173f8aa2dd | ||
|
|
aa9e9dd96e | ||
|
|
dd6601b9e5 | ||
|
|
78d8f9afb7 | ||
|
|
487bee0769 | ||
|
|
ab35bac204 | ||
|
|
30ea70edab | ||
|
|
eb4e7f9829 | ||
|
|
104935b443 | ||
|
|
1936211f8e | ||
|
|
87d0dbdf70 | ||
|
|
7bc28caf27 | ||
|
|
7498bed378 | ||
|
|
d2475ae1aa | ||
|
|
12174676d3 | ||
|
|
1850d56ec1 | ||
|
|
92134e22a4 | ||
|
|
01fde07a48 | ||
|
|
16223ee9c3 | ||
|
|
b010126c19 | ||
|
|
166178b011 | ||
|
|
aecbce50e3 | ||
|
|
cc324b4705 | ||
|
|
2785f14f31 | ||
|
|
0d3988d499 | ||
|
|
44a1c40d41 | ||
|
|
bef224105c | ||
|
|
776f12c99b | ||
|
|
1305277ba2 | ||
|
|
85b5bbaae0 | ||
|
|
8519f6881d | ||
|
|
60ac8ae0f3 | ||
|
|
4e9a5575a6 | ||
|
|
1f11a3ae94 | ||
|
|
9be980ce0b | ||
|
|
2838174c65 | ||
|
|
445623db30 | ||
|
|
3216a3f0b9 | ||
|
|
87e25490b2 | ||
|
|
b4250036c6 | ||
|
|
4012d58dca | ||
|
|
75c184c5c3 | ||
|
|
b087dec2d9 | ||
|
|
f07fe8687d | ||
|
|
e81351bfa8 | ||
|
|
c34f3443f5 | ||
|
|
9f4ae7b8dc | ||
|
|
8bfee207b4 | ||
|
|
9bf5adc052 | ||
|
|
a18e219000 | ||
|
|
b7e4bfb375 | ||
|
|
66836b0522 | ||
|
|
59d47dfdf5 | ||
|
|
75e6c0d115 | ||
|
|
a093e38f7e | ||
|
|
7d535f29a8 | ||
|
|
2bfd67273b | ||
|
|
e656e9e325 | ||
|
|
e90fe50943 | ||
|
|
2aa3cd0670 | ||
|
|
a2143caf02 | ||
|
|
04c83fc20d | ||
|
|
c7c42cb207 | ||
|
|
8a90f8f7b6 | ||
|
|
0e01820ea4 | ||
|
|
e2d0e31453 | ||
|
|
669c558120 | ||
|
|
12794d36b3 | ||
|
|
0d7cba9657 | ||
|
|
215c9d5136 | ||
|
|
77210d1b5e | ||
|
|
441c4e8228 | ||
|
|
858cecfdde | ||
|
|
1047e84962 | ||
|
|
7fc0ed5c56 | ||
|
|
6840555473 | ||
|
|
b31fd465ad | ||
|
|
b1d2a89b2e | ||
|
|
5b2a1be24c | ||
|
|
7e69f90a6f | ||
|
|
f95bbff997 | ||
|
|
339f65f618 | ||
|
|
1ae8c6370c | ||
|
|
eb3b88288a | ||
|
|
9b8449a908 | ||
|
|
ff87e4c595 | ||
|
|
918c679d94 | ||
|
|
04e9a824b3 | ||
|
|
63f35f78a6 | ||
|
|
1378b8959d | ||
|
|
c5034324d2 | ||
|
|
7eb0828bca | ||
|
|
80fb92161d | ||
|
|
fc741b7390 | ||
|
|
153af016e7 | ||
|
|
d3a4e353b1 | ||
|
|
4a49db92c1 | ||
|
|
eea9354e2f | ||
|
|
e6d62a5e64 | ||
|
|
782962ce5a | ||
|
|
53f1d07f00 | ||
|
|
a1de7a2b24 | ||
|
|
bae92bcf08 | ||
|
|
df91f38323 | ||
|
|
622c4a8ff0 | ||
|
|
b27013765a | ||
|
|
53072e2c89 | ||
|
|
c3a2409957 | ||
|
|
093e16cad0 | ||
|
|
28fb7961df | ||
|
|
9c3991af6d | ||
|
|
912e0579a6 | ||
|
|
0c1b440f8b | ||
|
|
3b46c56bbd | ||
|
|
bcf2110804 | ||
|
|
7270d0807f | ||
|
|
d26a771207 | ||
|
|
ebf75c2f00 | ||
|
|
30175d941d | ||
|
|
b4161afe14 | ||
|
|
6f09f1c52c | ||
|
|
aabc07b0a9 | ||
|
|
dca316deae | ||
|
|
d429763b85 | ||
|
|
bae531e264 | ||
|
|
c63eba4d1c | ||
|
|
0db90c0997 | ||
|
|
d0b038ac72 | ||
|
|
691e7ff4f8 | ||
|
|
d860051d1a | ||
|
|
a166b2e6a4 | ||
|
|
85a81ca383 | ||
|
|
e521f9bb0f | ||
|
|
d2400de365 | ||
|
|
5a76aaf839 | ||
|
|
1dddbde053 | ||
|
|
3368872f9f | ||
|
|
cb15e32454 | ||
|
|
e85c3a4d70 | ||
|
|
ea99f66a5e | ||
|
|
6757fadee3 | ||
|
|
843f6b9a39 | ||
|
|
aa7a6dfcac | ||
|
|
73c5630634 | ||
|
|
9fe4d4ad84 | ||
|
|
3b4434467a | ||
|
|
e50f287598 | ||
|
|
ae640da631 | ||
|
|
87aa3ca701 | ||
|
|
12ecad34ba | ||
|
|
11a983f078 | ||
|
|
332e05278c | ||
|
|
9942acf8ff | ||
|
|
66c214c2b7 | ||
|
|
dfa01dbb7a | ||
|
|
9e2f0af9a6 | ||
|
|
3178db04e2 | ||
|
|
4f41eef936 | ||
|
|
8069b9e9ae | ||
|
|
4d879dabba | ||
|
|
20f6c5db3f | ||
|
|
9953820174 | ||
|
|
1c8b78066f | ||
|
|
4d3e51d115 | ||
|
|
4fc73cdde0 | ||
|
|
caeb6b807c | ||
|
|
315e248d63 | ||
|
|
e068c2d41f | ||
|
|
0488e3f943 | ||
|
|
2ed25fdbb4 | ||
|
|
90b00ae4ff | ||
|
|
bd8b1265c4 | ||
|
|
f8f024ae7c | ||
|
|
0f71490c61 | ||
|
|
6fe2ef679b | ||
|
|
73e7aa3639 | ||
|
|
cc6ca7633d | ||
|
|
64d2f2e81c | ||
|
|
e572892a48 | ||
|
|
3b4f4a739e | ||
|
|
8400e47cfc | ||
|
|
b7cb4e88c4 | ||
|
|
73c957e350 | ||
|
|
783918a403 | ||
|
|
d0bf2327e3 | ||
|
|
e63730d960 | ||
|
|
d50bb404b9 | ||
|
|
747d8cabc5 | ||
|
|
b415edcfe0 | ||
|
|
0b01211a34 | ||
|
|
13ee61d5cf | ||
|
|
a302df61d4 | ||
|
|
5be0f86c83 | ||
|
|
53f9bada4c | ||
|
|
ce7d55ffa8 | ||
|
|
a6ea43a842 | ||
|
|
eac29768ae | ||
|
|
7788498762 | ||
|
|
d944aed9d3 | ||
|
|
67318ac759 | ||
|
|
90999bfc24 | ||
|
|
7635830399 | ||
|
|
4fbd06e18b | ||
|
|
5247e2c2aa | ||
|
|
8e21de8de3 | ||
|
|
d976f48b7b | ||
|
|
9ed51a2d3d | ||
|
|
ff72bcf5ef | ||
|
|
4d934685a8 | ||
|
|
a616c0d4c0 | ||
|
|
b938e2d757 | ||
|
|
b4603b4fbc | ||
|
|
495c21b776 | ||
|
|
bc874fa8f4 | ||
|
|
86c45f13c3 | ||
|
|
46ae195ba6 | ||
|
|
75a85831ab | ||
|
|
95e9f31141 | ||
|
|
79ac38c84d | ||
|
|
e2b4a964e7 | ||
|
|
f5f4c026bc | ||
|
|
efc42f5f22 | ||
|
|
d201a56571 | ||
|
|
f57279bdb7 | ||
|
|
d967bbb126 | ||
|
|
2553b32196 | ||
|
|
3f6fc56b3e | ||
|
|
97da0c0869 | ||
|
|
0acc57d36d | ||
|
|
d1bef702d6 | ||
|
|
d20d4f038b | ||
|
|
b7039a7a69 | ||
|
|
8fda23435c | ||
|
|
4f4a1436ae | ||
|
|
a8324795e5 | ||
|
|
f347dcc97b | ||
|
|
df69c8b7c2 | ||
|
|
fd20e90bb5 | ||
|
|
1da35ec17f | ||
|
|
69f29cfb45 | ||
|
|
e047aedee1 | ||
|
|
d14236be4d | ||
|
|
c814300f96 | ||
|
|
3171dec6e9 | ||
|
|
e9b658d868 | ||
|
|
72b8bc3866 | ||
|
|
f3742adc9f | ||
|
|
c5fec283f7 | ||
|
|
2d9ad22102 | ||
|
|
d0eab7a09f | ||
|
|
4f6e6185fc | ||
|
|
256624f3d8 | ||
|
|
74de004e6c | ||
|
|
b7d81016e1 | ||
|
|
6bd2b364ec | ||
|
|
bc764aec5a | ||
|
|
d18eec4d1b | ||
|
|
ec68ca2ca8 | ||
|
|
c9076fef63 | ||
|
|
c770dc6205 | ||
|
|
da8c733939 | ||
|
|
c21bdaff43 | ||
|
|
dab8b111d3 | ||
|
|
521681c0f7 | ||
|
|
2e3f82b98a | ||
|
|
2816e8ee73 | ||
|
|
4441be6b7b | ||
|
|
b992ca9694 | ||
|
|
d7e100692b | ||
|
|
2c979a6fbd | ||
|
|
f8c883f58e |
@@ -1,15 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
install:
|
||||
- ps: Start-FileDownload 'http://slproweb.com/download/Win64OpenSSL-1_0_2d.exe'
|
||||
- ps: Start-Process "Win64OpenSSL-1_0_2d.exe" -ArgumentList "/silent /verysilent /sp- /suppressmsgboxes" -Wait
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rust-nightly-${env:TARGET}.exe" -FileName "rust-nightly.exe"
|
||||
- ps: .\rust-nightly.exe /VERYSILENT /NORESTART /DIR="C:\rust" | Out-Null
|
||||
- ps: $env:PATH="$env:PATH;C:\rust\bin"
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
build: false
|
||||
test_script:
|
||||
- env OPENSSL_LIB_DIR=C:/OpenSSL-Win64 OPENSSL_INCLUDE_DIR=C:/OpenSSL-Win64/include cargo build --verbose
|
||||
7
.clog.toml
Normal file
7
.clog.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[clog]
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
changelog = "CHANGELOG.md"
|
||||
|
||||
[sections]
|
||||
Style = ["style"]
|
||||
Documentation = ["docs"]
|
||||
8
.editorconfig
Normal file
8
.editorconfig
Normal file
@@ -0,0 +1,8 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
|
||||
[*.rs]
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: amousset
|
||||
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Code allowing to reproduce the bug.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- Lettre version
|
||||
- OS
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
12
.github/workflows/audit.yml
vendored
Normal file
12
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Security audit
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
151
.github/workflows/test.yml
vendored
Normal file
151
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
RUSTFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUSTDOCFLAGS: "--cfg lettre_ignore_tls_mismatch"
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: rustfmt / nightly-2024-09-01
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup default nightly-2024-09-01
|
||||
rustup component add rustfmt
|
||||
|
||||
- name: cargo fmt
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: clippy / stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
rustup component add clippy
|
||||
|
||||
- name: Run clippy
|
||||
run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
|
||||
check:
|
||||
name: check / stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust
|
||||
run: rustup update --no-self-update stable
|
||||
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install cargo hack
|
||||
run: cargo install cargo-hack --debug
|
||||
|
||||
- name: Check with cargo hack
|
||||
run: cargo hack check --feature-powerset --depth 3 --at-least-one-of aws-lc-rs,ring --at-least-one-of rustls-native-certs,webpki-roots
|
||||
|
||||
test:
|
||||
name: test / ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: stable
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: '1.74'
|
||||
rust: '1.74'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup default ${{ matrix.rust }}
|
||||
rustup update --no-self-update ${{ matrix.rust }}
|
||||
|
||||
- name: Setup cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Install postfix
|
||||
run: |
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive sudo apt-get -y install postfix
|
||||
|
||||
- name: Run SMTP server
|
||||
run: smtp-sink 2525 1000&
|
||||
|
||||
- name: Install coredns
|
||||
run: |
|
||||
wget -q https://github.com/coredns/coredns/releases/download/v1.8.6/coredns_1.8.6_linux_amd64.tgz
|
||||
tar xzf coredns_1.8.6_linux_amd64.tgz
|
||||
|
||||
- name: Start coredns
|
||||
run: |
|
||||
sudo ./coredns -conf testdata/coredns.conf &
|
||||
sudo systemctl stop systemd-resolved
|
||||
echo "nameserver 127.0.0.54" | sudo tee /etc/resolv.conf
|
||||
|
||||
- name: Install dkimverify
|
||||
run: sudo apt -y install python3-dkim
|
||||
|
||||
- name: Test with no default features
|
||||
run: cargo test --no-default-features
|
||||
|
||||
- name: Test with default features
|
||||
run: cargo test
|
||||
|
||||
- name: Test with all features (-native-tls)
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,boring-tls,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-boring-tls,tokio1-rustls,tracing
|
||||
|
||||
- name: Test with all features (-boring-tls)
|
||||
run: cargo test --no-default-features --features async-std1,async-std1-rustls,aws-lc-rs,rustls-native-certs,builder,dkim,file-transport,file-transport-envelope,hostname,mime03,native-tls,pool,rustls-native-certs,rustls,sendmail-transport,smtp-transport,tokio1,tokio1-native-tls,tokio1-rustls,tracing
|
||||
|
||||
# coverage:
|
||||
# name: Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# - uses: actions-rs/toolchain@v1
|
||||
# with:
|
||||
# toolchain: nightly
|
||||
# override: true
|
||||
# - run: sudo DEBIAN_FRONTEND=noninteractive apt-get -y install postfix
|
||||
# - run: smtp-sink 2525 1000&
|
||||
# - uses: actions-rs/cargo@v1
|
||||
# with:
|
||||
# command: test
|
||||
# args: --no-fail-fast
|
||||
# env:
|
||||
# CARGO_INCREMENTAL: "0"
|
||||
# RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Zno-landing-pads"
|
||||
# - id: coverage
|
||||
# uses: actions-rs/grcov@v0.1
|
||||
# - name: Coveralls upload
|
||||
# uses: coverallsapp/github-action@master
|
||||
# with:
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# path-to-lcov: ${{ steps.coverage.outputs.report }}
|
||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -1,3 +1,6 @@
|
||||
.project
|
||||
/target/
|
||||
/Cargo.lock
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
|
||||
37
.travis.yml
37
.travis.yml
@@ -1,37 +0,0 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
sudo: false
|
||||
cache:
|
||||
apt: true
|
||||
pip: true
|
||||
directories:
|
||||
- target/debug/deps
|
||||
- target/debug/build
|
||||
- target/release/deps
|
||||
- target/release/build
|
||||
install:
|
||||
- pip install 'travis-cargo<0.2' --user
|
||||
- export PATH=$HOME/.local/bin:$PATH
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- postfix
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
before_script:
|
||||
- smtp-sink 2525 1000&
|
||||
script:
|
||||
- travis-cargo build
|
||||
- travis-cargo test
|
||||
- travis-cargo doc
|
||||
after_success:
|
||||
- travis-cargo --only nightly bench
|
||||
- travis-cargo --only stable coveralls --no-sudo
|
||||
- travis-cargo --only stable doc-upload
|
||||
env:
|
||||
global:
|
||||
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="
|
||||
705
CHANGELOG.md
Normal file
705
CHANGELOG.md
Normal file
@@ -0,0 +1,705 @@
|
||||
<a name="v0.11.16"></a>
|
||||
### v0.11.16 (2025-05-12)
|
||||
|
||||
#### Features
|
||||
|
||||
* Always implement `Clone` for `AsyncFileTransport` ([#1075])
|
||||
|
||||
#### Changes
|
||||
|
||||
* `Tls`, `CertificateStore`, `TlsParameters`, `TlsParametersBuilder`, `Certificate` and `Identity`
|
||||
are now marked as deprecated when no TLS backend is enabled. They will be properly feature gated
|
||||
in lettre v0.12 ([#1084])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Gate `web-time` behind `cfg(target_arch = "wasm32")]` ([#1086])
|
||||
* Add missing `#[doc(cfg(...))]` attributes ([#1086])
|
||||
* Upgrade `webpki-roots` to v1 ([#1088])
|
||||
* Cleanup internal `TlsParameters` and `(Async)NetworkStream` structures ([#1082])
|
||||
* Feature gate internal `TransportBuilder::tls` to avoid recursive call site warnings ([#1083])
|
||||
* Fix workaround for embedding cargo script in rustdoc output ([#1077])
|
||||
* Fix `clippy::io_other_error` warnings ([#1078])
|
||||
* Upgrade semver compatible dependencies ([#1076], [#1079], [#1080])
|
||||
|
||||
[#1075]: https://github.com/lettre/lettre/pull/1075
|
||||
[#1076]: https://github.com/lettre/lettre/pull/1076
|
||||
[#1077]: https://github.com/lettre/lettre/pull/1077
|
||||
[#1078]: https://github.com/lettre/lettre/pull/1078
|
||||
[#1079]: https://github.com/lettre/lettre/pull/1079
|
||||
[#1080]: https://github.com/lettre/lettre/pull/1080
|
||||
[#1082]: https://github.com/lettre/lettre/pull/1082
|
||||
[#1083]: https://github.com/lettre/lettre/pull/1083
|
||||
[#1084]: https://github.com/lettre/lettre/pull/1084
|
||||
[#1086]: https://github.com/lettre/lettre/pull/1086
|
||||
[#1088]: https://github.com/lettre/lettre/pull/1088
|
||||
|
||||
<a name="v0.11.15"></a>
|
||||
### v0.11.15 (2025-03-10)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.74 ([#1060])
|
||||
|
||||
#### Features
|
||||
|
||||
* Add controlled shutdown methods ([#1045], [#1068])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Deny `unreachable_pub` lint ([#1058])
|
||||
* Bump minimum supported `rustls` ([#1063])
|
||||
* Bump minimum supported `serde` ([#1064])
|
||||
* Upgrade semver compatible dependencies ([#1067])
|
||||
* Upgrade `email-encoding` to v0.4 ([#1069])
|
||||
|
||||
[#1045]: https://github.com/lettre/lettre/pull/1045
|
||||
[#1058]: https://github.com/lettre/lettre/pull/1058
|
||||
[#1060]: https://github.com/lettre/lettre/pull/1060
|
||||
[#1063]: https://github.com/lettre/lettre/pull/1063
|
||||
[#1064]: https://github.com/lettre/lettre/pull/1064
|
||||
[#1067]: https://github.com/lettre/lettre/pull/1067
|
||||
[#1068]: https://github.com/lettre/lettre/pull/1068
|
||||
[#1069]: https://github.com/lettre/lettre/pull/1069
|
||||
|
||||
<a name="v0.11.14"></a>
|
||||
### v0.11.14 (2025-02-23)
|
||||
|
||||
This release deprecates the `rustls-tls`, `tokio1-rustls-tls` and `async-std1-rustls-tls`
|
||||
features, which will be removed in lettre v0.12.
|
||||
|
||||
rustls users should start migrating to the `rustls`, `tokio1-rustls` and
|
||||
`async-std1-rustls` features. Unlike the deprecated _*rustls-tls_ features,
|
||||
which automatically enabled the `ring` and `webpki-roots` backends, the new
|
||||
features do not. To complete the migration, users must either enable the
|
||||
`aws-lc-rs` or the `ring` feature. Additionally, those who rely on `webpki-roots`
|
||||
for TLS certificate verification must now explicitly enable its feature.
|
||||
Users of `rustls-native-certs` do not need to enable `webpki-roots`.
|
||||
|
||||
Find out more about the new features via the [lettre rustls docs].
|
||||
|
||||
#### Features
|
||||
|
||||
* Make it possible to use different `rustls` crypto providers and TLS verifiers ([#1054])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Use the same `rustls` crypto provider everywhere ([#1055])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Deprecate `AsyncNetworkStream` being public ([#1059])
|
||||
* Upgrade `nom` to v8 ([#1048])
|
||||
* Drop `rustls-pemfile` in favor of `rustls-pki-types` APIs ([#1050])
|
||||
* Ban direct use of `std::time::SystemTime::now` via clippy ([#1043])
|
||||
* Drop direct dependency on `rustls-pki-types` ([#1051])
|
||||
* Remove artifact from `web-time` refactor ([#1049])
|
||||
* Fix warnings with `rustls-native-certs` when `tracing` is disabled ([#1053])
|
||||
* Bump license year ([#1057])
|
||||
* Cleanup `Cargo.toml` style ([#1047])
|
||||
|
||||
[lettre rustls docs]: https://docs.rs/lettre/0.11.14/lettre/index.html#smtp-over-tls-via-the-rustls-crate
|
||||
[#1043]: https://github.com/lettre/lettre/pull/1043
|
||||
[#1047]: https://github.com/lettre/lettre/pull/1047
|
||||
[#1048]: https://github.com/lettre/lettre/pull/1048
|
||||
[#1049]: https://github.com/lettre/lettre/pull/1049
|
||||
[#1050]: https://github.com/lettre/lettre/pull/1050
|
||||
[#1051]: https://github.com/lettre/lettre/pull/1051
|
||||
[#1053]: https://github.com/lettre/lettre/pull/1053
|
||||
[#1054]: https://github.com/lettre/lettre/pull/1054
|
||||
[#1055]: https://github.com/lettre/lettre/pull/1055
|
||||
[#1057]: https://github.com/lettre/lettre/pull/1057
|
||||
[#1059]: https://github.com/lettre/lettre/pull/1059
|
||||
|
||||
<a name="v0.11.13"></a>
|
||||
### v0.11.13 (2025-02-17)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add WASM support ([#1037], [#1042])
|
||||
* Add method to get the TLS verify result with BoringSSL ([#1039])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Synchronous pool shutdowns being arbitrarily delayed ([#1041])
|
||||
|
||||
[#1037]: https://github.com/lettre/lettre/pull/1037
|
||||
[#1039]: https://github.com/lettre/lettre/pull/1039
|
||||
[#1041]: https://github.com/lettre/lettre/pull/1041
|
||||
[#1042]: https://github.com/lettre/lettre/pull/1042
|
||||
|
||||
<a name="v0.11.12"></a>
|
||||
### v0.11.12 (2025-02-02)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Warn against manually configuring `port` and `tls` on SMTP transport builder ([#1014])
|
||||
* Document variants of `Tls` enum ([#1015])
|
||||
* Fix rustdoc warnings ([#1016])
|
||||
* Add `ContentType::TEXT_PLAIN` to `Message` builder examples ([#1017])
|
||||
* Document `SmtpTransport` and `AsyncSmtpTransport` ([#1018])
|
||||
* Fix typo in transport builder `credentials` method ([#1019])
|
||||
* Document required system dependencies for OpenSSL ([#1030])
|
||||
* Improve docs for the `transport::smtp` module ([#1031])
|
||||
* Improve docs for smtp transport builder `from_url` ([#1032])
|
||||
* Replace `assert!` with `?` on `send` examples ([#1033])
|
||||
* Warn on more pedantic clippy lints and fix them ([#1035], [#1036])
|
||||
|
||||
[#1014]: https://github.com/lettre/lettre/pull/1014
|
||||
[#1015]: https://github.com/lettre/lettre/pull/1015
|
||||
[#1016]: https://github.com/lettre/lettre/pull/1016
|
||||
[#1017]: https://github.com/lettre/lettre/pull/1017
|
||||
[#1018]: https://github.com/lettre/lettre/pull/1018
|
||||
[#1019]: https://github.com/lettre/lettre/pull/1019
|
||||
[#1030]: https://github.com/lettre/lettre/pull/1030
|
||||
[#1031]: https://github.com/lettre/lettre/pull/1031
|
||||
[#1032]: https://github.com/lettre/lettre/pull/1032
|
||||
[#1033]: https://github.com/lettre/lettre/pull/1033
|
||||
[#1035]: https://github.com/lettre/lettre/pull/1035
|
||||
[#1036]: https://github.com/lettre/lettre/pull/1036
|
||||
|
||||
<a name="v0.11.11"></a>
|
||||
### v0.11.11 (2024-12-05)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.71 ([#1008])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix off-by-one error reaching the minimum number of configured pooled connections ([#1012])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Fix clippy warnings ([#1009])
|
||||
* Fix `-Zminimal-versions` build ([#1007])
|
||||
|
||||
[#1007]: https://github.com/lettre/lettre/pull/1007
|
||||
[#1008]: https://github.com/lettre/lettre/pull/1008
|
||||
[#1009]: https://github.com/lettre/lettre/pull/1009
|
||||
[#1012]: https://github.com/lettre/lettre/pull/1012
|
||||
|
||||
<a name="v0.11.10"></a>
|
||||
### v0.11.10 (2024-10-23)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Ignore disconnect errors when `pool` feature of SMTP transport is disabled ([#999])
|
||||
* Use case insensitive comparisons for matching login challenge requests ([#1000])
|
||||
|
||||
[#999]: https://github.com/lettre/lettre/pull/999
|
||||
[#1000]: https://github.com/lettre/lettre/pull/1000
|
||||
|
||||
<a name="v0.11.9"></a>
|
||||
### v0.11.9 (2024-09-13)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix feature gate for `accept_invalid_hostnames` for rustls ([#988])
|
||||
* Fix parsing `Mailbox` with trailing spaces ([#986])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `rustls-native-certs` to v0.8 ([#992])
|
||||
* Make getting started example in readme complete ([#990])
|
||||
|
||||
[#988]: https://github.com/lettre/lettre/pull/988
|
||||
[#986]: https://github.com/lettre/lettre/pull/986
|
||||
[#990]: https://github.com/lettre/lettre/pull/990
|
||||
[#992]: https://github.com/lettre/lettre/pull/992
|
||||
|
||||
<a name="v0.11.8"></a>
|
||||
### v0.11.8 (2024-09-03)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add mTLS support ([#974])
|
||||
* Implement `accept_invalid_hostnames` for rustls ([#977])
|
||||
* Provide certificate chain for peer certificates when using `rustls` or `boring-tls` ([#976])
|
||||
|
||||
#### Changes
|
||||
|
||||
* Make `HeaderName` comparisons via `PartialEq` case insensitive ([#980])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Fix clippy warnings ([#979])
|
||||
* Replace manual impl of `#[non_exhaustive]` for `InvalidHeaderName` ([#981])
|
||||
|
||||
[#974]: https://github.com/lettre/lettre/pull/974
|
||||
[#976]: https://github.com/lettre/lettre/pull/976
|
||||
[#977]: https://github.com/lettre/lettre/pull/977
|
||||
[#980]: https://github.com/lettre/lettre/pull/980
|
||||
[#981]: https://github.com/lettre/lettre/pull/981
|
||||
|
||||
<a name="v0.11.7"></a>
|
||||
### v0.11.7 (2024-04-23)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `hostname` to v0.4 ([#956])
|
||||
* Fix `tracing` message consistency ([#960])
|
||||
* Bump minimum required `rustls` to v0.23.5 ([#958])
|
||||
* Dropped use of `ref` syntax in the entire project ([#959])
|
||||
|
||||
[#956]: https://github.com/lettre/lettre/pull/956
|
||||
[#958]: https://github.com/lettre/lettre/pull/958
|
||||
[#959]: https://github.com/lettre/lettre/pull/959
|
||||
[#960]: https://github.com/lettre/lettre/pull/960
|
||||
|
||||
<a name="v0.11.6"></a>
|
||||
### v0.11.6 (2024-03-28)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Upgraded `email-encoding` to v0.3 - fixing multiple encoding bugs in the process ([#952])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Updated copyright year in license ([#954])
|
||||
|
||||
[#952]: https://github.com/lettre/lettre/pull/952
|
||||
[#954]: https://github.com/lettre/lettre/pull/954
|
||||
|
||||
<a name="v0.11.5"></a>
|
||||
### v0.11.5 (2024-03-25)
|
||||
|
||||
#### Features
|
||||
|
||||
* Support SMTP SASL draft login challenge ([#911])
|
||||
* Add conversion from SMTP response code to integer ([#941])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `rustls` to v0.23 ([#950])
|
||||
* Bump `base64` to v0.22 ([#945])
|
||||
* Fix typos in documentation ([#943], [#944])
|
||||
* Add `Cargo.lock` ([#942])
|
||||
|
||||
[#911]: https://github.com/lettre/lettre/pull/911
|
||||
[#941]: https://github.com/lettre/lettre/pull/941
|
||||
[#942]: https://github.com/lettre/lettre/pull/942
|
||||
[#943]: https://github.com/lettre/lettre/pull/943
|
||||
[#944]: https://github.com/lettre/lettre/pull/944
|
||||
[#945]: https://github.com/lettre/lettre/pull/945
|
||||
[#950]: https://github.com/lettre/lettre/pull/950
|
||||
|
||||
<a name="v0.11.4"></a>
|
||||
### v0.11.4 (2024-01-28)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Percent decode credentials in SMTP connect URL ([#932], [#934])
|
||||
* Fix mimebody DKIM body-hash computation ([#923])
|
||||
|
||||
[#923]: https://github.com/lettre/lettre/pull/923
|
||||
[#932]: https://github.com/lettre/lettre/pull/932
|
||||
[#934]: https://github.com/lettre/lettre/pull/934
|
||||
|
||||
<a name="v0.11.3"></a>
|
||||
### v0.11.3 (2024-01-02)
|
||||
|
||||
#### Features
|
||||
|
||||
* Derive `Clone` for `FileTransport` and `AsyncFileTransport` ([#924])
|
||||
* Derive `Debug` for `SmtpTransport` ([#925])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Upgrade `rustls` to v0.22 ([#921])
|
||||
* Drop once_cell dependency in favor of OnceLock from std ([#928])
|
||||
|
||||
[#921]: https://github.com/lettre/lettre/pull/921
|
||||
[#924]: https://github.com/lettre/lettre/pull/924
|
||||
[#925]: https://github.com/lettre/lettre/pull/925
|
||||
[#928]: https://github.com/lettre/lettre/pull/928
|
||||
|
||||
<a name="v0.11.2"></a>
|
||||
### v0.11.2 (2023-11-23)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.70 ([#916])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `idna` to v0.5 ([#918])
|
||||
* Bump `boring` and `tokio-boring` to v4 ([#915])
|
||||
|
||||
[#915]: https://github.com/lettre/lettre/pull/915
|
||||
[#916]: https://github.com/lettre/lettre/pull/916
|
||||
[#918]: https://github.com/lettre/lettre/pull/918
|
||||
|
||||
<a name="v0.11.1"></a>
|
||||
### v0.11.1 (2023-10-24)
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Fix `webpki-roots` certificate store setup ([#909])
|
||||
|
||||
[#909]: https://github.com/lettre/lettre/pull/909
|
||||
|
||||
<a name="v0.11.0"></a>
|
||||
### v0.11.0 (2023-10-15)
|
||||
|
||||
While this release technically contains breaking changes, we expect most projects
|
||||
to be able to upgrade by only bumping the version in `Cargo.toml`.
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.65 ([#869] and [#881])
|
||||
* `AddressError` is now marked as `#[non_exhaustive]` ([#839])
|
||||
|
||||
#### Features
|
||||
|
||||
* Improve mailbox parsing ([#839])
|
||||
* Add construction of SMTP transport from URL ([#901])
|
||||
* Add `From<Address>` implementation for `Mailbox` ([#879])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bump `socket2` to v0.5 ([#868])
|
||||
* Bump `idna` to v0.4, `fastrand` to v2, `quoted_printable` to v0.5, `rsa` to v0.9 ([#882])
|
||||
* Bump `webpki-roots` to v0.25 ([#884] and [#890])
|
||||
* Bump `ed25519-dalek` to v2 fixing RUSTSEC-2022-0093 ([#896])
|
||||
* Bump `boring`ssl crates to v3 ([#897])
|
||||
|
||||
[#839]: https://github.com/lettre/lettre/pull/839
|
||||
[#868]: https://github.com/lettre/lettre/pull/868
|
||||
[#869]: https://github.com/lettre/lettre/pull/869
|
||||
[#879]: https://github.com/lettre/lettre/pull/879
|
||||
[#881]: https://github.com/lettre/lettre/pull/881
|
||||
[#882]: https://github.com/lettre/lettre/pull/882
|
||||
[#884]: https://github.com/lettre/lettre/pull/884
|
||||
[#890]: https://github.com/lettre/lettre/pull/890
|
||||
[#896]: https://github.com/lettre/lettre/pull/896
|
||||
[#897]: https://github.com/lettre/lettre/pull/897
|
||||
[#901]: https://github.com/lettre/lettre/pull/901
|
||||
|
||||
<a name="v0.10.4"></a>
|
||||
### v0.10.4 (2023-04-02)
|
||||
|
||||
#### Misc
|
||||
|
||||
* Bumped rustls to 0.21 and all related dependencies ([#867])
|
||||
|
||||
[#867]: https://github.com/lettre/lettre/pull/867
|
||||
|
||||
<a name="v0.10.3"></a>
|
||||
### v0.10.3 (2023-02-20)
|
||||
|
||||
#### Announcements
|
||||
|
||||
It was found that what had been used until now as a basic lettre 0.10
|
||||
`MessageBuilder::body` example failed to mention that for maximum
|
||||
compatibility with various email clients a `Content-Type` header
|
||||
should always be present in the message.
|
||||
|
||||
##### Before
|
||||
|
||||
```rust
|
||||
Message::builder()
|
||||
// [...] some headers skipped for brevity
|
||||
.body(String::from("A plaintext or html body"))?
|
||||
```
|
||||
|
||||
##### Patch
|
||||
|
||||
```diff
|
||||
Message::builder()
|
||||
// [...] some headers skipped for brevity
|
||||
+ .header(ContentType::TEXT_PLAIN) // or `TEXT_HTML` if the body is html
|
||||
.body(String::from("A plaintext or html body"))?
|
||||
```
|
||||
|
||||
#### Features
|
||||
|
||||
* Add support for rustls-native-certs when using rustls ([#843])
|
||||
|
||||
[#843]: https://github.com/lettre/lettre/pull/843
|
||||
|
||||
<a name="v0.10.2"></a>
|
||||
### v0.10.2 (2023-01-29)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
* MSRV is now 1.60 ([#828])
|
||||
|
||||
#### Features
|
||||
|
||||
* Allow providing a custom `tokio` stream for `AsyncSmtpTransport` ([#805])
|
||||
* Return whole SMTP error message ([#821])
|
||||
|
||||
#### Bug fixes
|
||||
|
||||
* Mailbox displays wrongly when containing a comma and a non-ascii char in its name ([#827])
|
||||
* Require `quoted_printable` ^0.4.6 in order to fix encoding of tabs and spaces at the end of line ([#837])
|
||||
|
||||
#### Misc
|
||||
|
||||
* Increase tracing ([#848])
|
||||
* Bump `idna` to 0.3 ([#816])
|
||||
* Update `base64` to 0.21 ([#840] and [#851])
|
||||
* Update `rsa` to 0.8 ([#829] and [#852])
|
||||
|
||||
[#805]: https://github.com/lettre/lettre/pull/805
|
||||
[#816]: https://github.com/lettre/lettre/pull/816
|
||||
[#821]: https://github.com/lettre/lettre/pull/821
|
||||
[#827]: https://github.com/lettre/lettre/pull/827
|
||||
[#828]: https://github.com/lettre/lettre/pull/828
|
||||
[#829]: https://github.com/lettre/lettre/pull/829
|
||||
[#837]: https://github.com/lettre/lettre/pull/837
|
||||
[#840]: https://github.com/lettre/lettre/pull/840
|
||||
[#848]: https://github.com/lettre/lettre/pull/848
|
||||
[#851]: https://github.com/lettre/lettre/pull/851
|
||||
[#852]: https://github.com/lettre/lettre/pull/852
|
||||
|
||||
<a name="v0.10.1"></a>
|
||||
### v0.10.1 (2022-07-20)
|
||||
|
||||
#### Features
|
||||
|
||||
* Add `boring-tls` support for `SmtpTransport` and `AsyncSmtpTransport`. The latter is only supported with the tokio runtime. ([#797]) ([#798])
|
||||
* Make the minimum TLS version configurable. ([#799]) ([#800])
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Ensure connections are closed on abort. ([#801])
|
||||
* Fix SMTP dot stuffing. ([#803])
|
||||
|
||||
[#797]: https://github.com/lettre/lettre/pull/797
|
||||
[#798]: https://github.com/lettre/lettre/pull/798
|
||||
[#799]: https://github.com/lettre/lettre/pull/799
|
||||
[#800]: https://github.com/lettre/lettre/pull/800
|
||||
[#801]: https://github.com/lettre/lettre/pull/801
|
||||
[#803]: https://github.com/lettre/lettre/pull/803
|
||||
|
||||
<a name="v0.10.0"></a>
|
||||
### v0.10.0 (2022-06-29)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
||||
|
||||
* MSRV is now 1.56.0
|
||||
* The `lettre_email` crate has been merged into `lettre`. To migrate, replace `lettre_email` with `lettre::message`
|
||||
and make sure to enable the `builder` feature (it's enabled by default).
|
||||
* `SendableEmail` has been renamed to `Email` and `EmailBuilder::build()` produces it directly. To migrate,
|
||||
rename `SendableEmail` to `Email`.
|
||||
* The `serde-impls` feature has been renamed to `serde`. To migrate, rename the feature.
|
||||
|
||||
#### Features
|
||||
|
||||
* Add `tokio` 1 support
|
||||
* Add `rustls` support
|
||||
* Add `async-std` support. NOTE: native-tls isn't supported when using async-std for the smtp transport.
|
||||
* Allow enabling multiple SMTP authentication mechanisms
|
||||
* Allow providing a custom message id
|
||||
* Allow sending raw emails
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
* Merge `lettre_email` into `lettre`
|
||||
* Merge `Email` and `SendableEmail` into `lettre::message::Email`
|
||||
* SmtpTransport is now an high level SMTP client. It provides connection pooling and shortcuts for building clients using commonly desired values
|
||||
* Refactor `TlsParameters` implementation to not expose the internal TLS library
|
||||
* `FileTransport` writes emails into `.eml` instead of `.json`
|
||||
* When the hostname feature is disabled or hostname cannot be fetched, `127.0.0.1` is used instead of `localhost` as EHLO parameter (for better RFC compliance and mail server compatibility)
|
||||
* The `sendmail` and `file` transports aren't enabled by default anymore.
|
||||
* The `new` method of `ClientId` is deprecated
|
||||
* Rename `serde-impls` feature to `serde`
|
||||
* The `SendmailTransport` now uses the `sendmail` command in current `PATH` by default instead of
|
||||
`/usr/bin/sendmail`.
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md))
|
||||
* Correctly encode header values containing non-ASCII characters
|
||||
* Timeout bug causing infinite hang
|
||||
* Fix doc tests in website
|
||||
* Fix docs for `domain` field
|
||||
|
||||
#### Misc
|
||||
|
||||
* Improve documentation, examples and tests
|
||||
* Replace `line-wrap`, `email`, `bufstream` with our own implementations
|
||||
* Remove `bytes`
|
||||
* Remove `time`
|
||||
* Remove `fast_chemail`
|
||||
* Update `base64` to 0.13
|
||||
* Update `hostname` to 0.3
|
||||
* Update to `nom` 6
|
||||
* Replace `log` with `tracing`
|
||||
* Move CI to GitHub Actions
|
||||
* Use criterion for benchmarks
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
### v0.9.2 (2019-06-11)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **email:**
|
||||
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
|
||||
|
||||
<a name="v0.9.1"></a>
|
||||
### v0.9.1 (2019-05-05)
|
||||
|
||||
#### Features
|
||||
|
||||
* **email:**
|
||||
* Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9))
|
||||
|
||||
<a name="v0.9.0"></a>
|
||||
### v0.9.0 (2019-03-17)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **email:**
|
||||
* Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69))
|
||||
* Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe))
|
||||
|
||||
* **transport:**
|
||||
* Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560))
|
||||
* Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8))
|
||||
|
||||
#### Features
|
||||
|
||||
* **all:**
|
||||
* Update dependencies
|
||||
* Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d))
|
||||
|
||||
* **transport:**
|
||||
* Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb))
|
||||
* Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164))
|
||||
* Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2))
|
||||
* SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e))
|
||||
* Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34))
|
||||
* Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a))
|
||||
|
||||
* **email:**
|
||||
* Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6))
|
||||
* Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9))
|
||||
|
||||
<a name="v0.8.2"></a>
|
||||
### v0.8.2 (2018-05-03)
|
||||
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **transport:** Write timeout is not set in smtp transport ([cc3580a8](https://github.com/lettre/lettre/commit/cc3580a8942e11c2addf6677f05e16fb451c7ea0))
|
||||
|
||||
#### Style
|
||||
|
||||
* **all:** Fix typos ([360c42ff](https://github.com/lettre/lettre/commit/360c42ffb8f706222eaad14e72619df1e4857814))
|
||||
|
||||
#### Features
|
||||
|
||||
* **all:**
|
||||
* Add set -xe option to build scripts ([57bbabaa](https://github.com/lettre/lettre/commit/57bbabaa6a10cc1a4de6f379e25babfee7adf6ad))
|
||||
* Move post-success scripts to separate files ([3177b58c](https://github.com/lettre/lettre/commit/3177b58c6d11ffae73c958713f6f0084173924e1))
|
||||
* Add website upload to travis build script ([a5294df6](https://github.com/lettre/lettre/commit/a5294df63728e14e24eeb851bb4403abd6a7bd36))
|
||||
* Add codecov upload in travis ([a03bfa00](https://github.com/lettre/lettre/commit/a03bfa008537b1d86ff789d0823e89ad5d99bd79))
|
||||
* Update README to put useful links at the top ([1ebbe660](https://github.com/lettre/lettre/commit/1ebbe660f5e142712f702c02d5d1e45211763b42))
|
||||
* Update badges in README and Cargo.toml ([f7ee5c42](https://github.com/lettre/lettre/commit/f7ee5c427ad71e4295f2f1d8e3e9e2dd850223e8))
|
||||
* Move docs from hugo to gitbook ([27935e32](https://github.com/lettre/lettre/commit/27935e32ef097db8db004569f35cad1d6cd30eca))
|
||||
* **transport:** Use md-5 and hmac instead of rust-crypto ([0cf018a8](https://github.com/lettre/lettre/commit/0cf018a85e4ea1ad16c7216670da560cc915ec32))
|
||||
|
||||
|
||||
|
||||
<a name="v0.8.1"></a>
|
||||
### v0.8.1 (2018-04-11)
|
||||
|
||||
#### Fix
|
||||
|
||||
* **all:**
|
||||
* Replace skeptic by some custom rustdoc invocations ([81bad131](https://github.com/lettre/lettre/commit/81bad1317519d330c46ea02f2b7a266b97cc00dd))
|
||||
|
||||
#### Documentation
|
||||
|
||||
* **all:**
|
||||
* Add changelog sections for style and docs ([b4d03ead](https://github.com/lettre/lettre/commit/b4d03ead8cce04e0c3d65a30e7a07acca9530f30))
|
||||
* Use clog to generate changelogs ([8981a775](https://github.com/lettre/lettre/commit/8981a7758c89be69974ef204c4390744aea94e4f), closes [#233](https://github.com/lettre/lettre/issues/233))
|
||||
|
||||
#### Style
|
||||
|
||||
* **transport-smtp:** Avoid useless empty format strings ([f3271715](https://github.com/lettre/lettre/commit/f3271715ecaf2793c9064462184867e4f22b0ead))
|
||||
|
||||
|
||||
|
||||
<a name="v0.8.0"></a>
|
||||
### v0.8.0 (2018-03-31)
|
||||
|
||||
#### Added
|
||||
|
||||
* Support binary files as attachment
|
||||
* Move doc to a dedicated website
|
||||
* Add tests for the doc using skeptic
|
||||
* Added a code of conduct
|
||||
* Use hostname as `ClientId` when available
|
||||
|
||||
#### Changed
|
||||
|
||||
* Detail in SMTP Response is now an enum
|
||||
* Use nom for parsing smtp responses
|
||||
* `Envelope` was moved from `lettre_email` to `lettre`
|
||||
* `EmailAddress::new()` now returns a `Result`
|
||||
* `SendableEmail` replaces `from` and `to` by `envelope` that returns an `Envelope`
|
||||
* `File` transport storage format has changed
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Add missing "Bcc" headers when building the email
|
||||
* Specify utf-8 charset for html
|
||||
* Use parts for text and html methods to work with attachments
|
||||
|
||||
#### Removed
|
||||
|
||||
* `get_ehlo` and `reset` in SmtpTransport are now private
|
||||
|
||||
<a name="v0.7.0"></a>
|
||||
### v0.7.0 (2017-10-08)
|
||||
|
||||
#### Added
|
||||
|
||||
* Allow validating server certificate
|
||||
* Initial (incomplete) attachments support
|
||||
|
||||
#### Changed
|
||||
|
||||
* Split into the *lettre* and *lettre_email* crates
|
||||
* A lot of small improvements
|
||||
* Use *tls-native* instead of *openssl* in smtp transport
|
||||
|
||||
<a name="v0.6.2"></a>
|
||||
### v0.6.2 (2017-02-18)
|
||||
|
||||
#### Changed
|
||||
|
||||
* Update env-logger crate to 0.4
|
||||
* Update openssl crate to 0.9
|
||||
* Update uuid crate to 0.4
|
||||
|
||||
<a name="v0.6.1"></a>
|
||||
### v0.6.1 (2016-10-19)
|
||||
|
||||
#### Changes
|
||||
|
||||
* **documentation**
|
||||
* #91: Build separate docs for each release
|
||||
* #96: Add complete documentation information to README
|
||||
|
||||
#### Fixed
|
||||
|
||||
* #85: Use address-list for "To", "From" etc.
|
||||
* #93: Force building tests before coverage computing
|
||||
|
||||
<a name="v0.6.0"></a>
|
||||
### v0.6.0 (2016-05-05)
|
||||
|
||||
#### Changes
|
||||
|
||||
* multipart support
|
||||
* add non-consuming methods for Email builders
|
||||
* `add_header` does not return the builder anymore,
|
||||
for consistency with other methods. Use the `header`
|
||||
method instead
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[version]: https://www.contributor-covenant.org/version/1/4/
|
||||
43
CONTRIBUTING.md
Normal file
43
CONTRIBUTING.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## Contributing to Lettre
|
||||
|
||||
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Code formatting
|
||||
|
||||
All code must be formatted using `rustfmt`.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer>
|
||||
```
|
||||
|
||||
Any line of the commit message cannot be longer 72 characters.
|
||||
|
||||
**type** must be one of the following:
|
||||
|
||||
feat: A new feature
|
||||
fix: A bug fix
|
||||
docs: Documentation only changes
|
||||
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
perf: A code change that improves performance
|
||||
|
||||
**scope** is the lettre part that is being touched. Examples:
|
||||
|
||||
email
|
||||
transport-smtp
|
||||
transport-file
|
||||
transport
|
||||
all
|
||||
|
||||
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.
|
||||
|
||||
### Release process
|
||||
|
||||
Releases are made using `cargo-release`:
|
||||
|
||||
```bash
|
||||
cargo release --dry-run 0.10.0 --prev-tag-name v0.9.2 -v
|
||||
```
|
||||
3057
Cargo.lock
generated
Normal file
3057
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
193
Cargo.toml
193
Cargo.toml
@@ -1,27 +1,190 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.5.1"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.11.16"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
documentation = "http://lettre.github.io/lettre/"
|
||||
homepage = "https://lettre.rs"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2021"
|
||||
rust-version = "1.74"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
time = "0.1"
|
||||
uuid = "0.1"
|
||||
log = "0.3"
|
||||
rustc-serialize = "0.3"
|
||||
rust-crypto = "0.2"
|
||||
bufstream = "0.1"
|
||||
email = "0.0"
|
||||
openssl = "0.7"
|
||||
email_address = { version = "0.2.1", default-features = false }
|
||||
chumsky = "0.9"
|
||||
idna = "1"
|
||||
|
||||
## tracing support
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true }
|
||||
|
||||
# builder
|
||||
httpdate = { version = "1", optional = true }
|
||||
mime = { version = "0.3.4", optional = true }
|
||||
fastrand = { version = "2.0", optional = true }
|
||||
quoted_printable = { version = "0.5", optional = true }
|
||||
base64 = { version = "0.22", optional = true }
|
||||
email-encoding = { version = "0.4", optional = true }
|
||||
|
||||
# file transport
|
||||
uuid = { version = "1", features = ["v4"], optional = true }
|
||||
serde = { version = "1.0.110", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp-transport
|
||||
nom = { version = "8", optional = true }
|
||||
hostname = { version = "0.4", optional = true } # feature
|
||||
socket2 = { version = "0.5.1", optional = true }
|
||||
url = { version = "2.4", optional = true }
|
||||
percent-encoding = { version = "2.3", optional = true }
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2.9", optional = true } # feature
|
||||
rustls = { version = "0.23.18", default-features = false, features = ["logging", "std", "tls12"], optional = true }
|
||||
rustls-platform-verifier = { version = "0.6.0", optional = true }
|
||||
rustls-native-certs = { version = "0.8", optional = true }
|
||||
webpki-roots = { version = "1.0.0", optional = true }
|
||||
boring = { version = "4", optional = true }
|
||||
|
||||
# async
|
||||
futures-io = { version = "0.3.7", optional = true }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["io"], optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
|
||||
## async-std
|
||||
async-std = { version = "1.8", optional = true }
|
||||
futures-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||
|
||||
## tokio
|
||||
tokio1_crate = { package = "tokio", version = "1", optional = true }
|
||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.26", default-features = false, features = ["logging", "tls12"], optional = true }
|
||||
tokio1_boring = { package = "tokio-boring", version = "4", optional = true }
|
||||
|
||||
## dkim
|
||||
sha2 = { version = "0.10", features = ["oid"], optional = true }
|
||||
rsa = { version = "0.9", optional = true }
|
||||
ed25519-dalek = { version = "2", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
## web-time for wasm support
|
||||
web-time = { version = "1.1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.3"
|
||||
pretty_assertions = "1"
|
||||
criterion = "0.5"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"] }
|
||||
tracing-subscriber = "0.3"
|
||||
glob = "0.3"
|
||||
walkdir = "2"
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
async-std = { version = "1.8", features = ["attributes"] }
|
||||
serde_json = "1"
|
||||
maud = "0.26"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "mailbox_parsing"
|
||||
|
||||
[features]
|
||||
unstable = []
|
||||
default = ["smtp-transport", "pool", "native-tls", "hostname", "builder"]
|
||||
builder = ["dep:httpdate", "dep:mime", "dep:fastrand", "dep:quoted_printable", "dep:email-encoding"]
|
||||
mime03 = ["dep:mime"]
|
||||
|
||||
# transports
|
||||
file-transport = ["dep:uuid", "tokio1_crate?/fs", "tokio1_crate?/io-util"]
|
||||
file-transport-envelope = ["serde", "dep:serde_json", "file-transport"]
|
||||
sendmail-transport = ["tokio1_crate?/process", "tokio1_crate?/io-util", "async-std?/unstable"]
|
||||
smtp-transport = ["dep:base64", "dep:nom", "dep:socket2", "dep:url", "dep:percent-encoding", "tokio1_crate?/rt", "tokio1_crate?/time", "tokio1_crate?/net"]
|
||||
|
||||
pool = ["dep:futures-util"]
|
||||
|
||||
rustls = ["dep:rustls"]
|
||||
aws-lc-rs = ["rustls?/aws-lc-rs"]
|
||||
fips = ["aws-lc-rs", "rustls?/fips"]
|
||||
ring = ["rustls?/ring"]
|
||||
webpki-roots = ["dep:webpki-roots"]
|
||||
# deprecated
|
||||
rustls-tls = ["webpki-roots", "rustls", "ring"]
|
||||
|
||||
boring-tls = ["dep:boring"]
|
||||
|
||||
# async
|
||||
async-std1 = ["dep:async-std", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
async-std1-rustls = ["async-std1", "rustls", "dep:futures-rustls"]
|
||||
# deprecated
|
||||
async-std1-rustls-tls = ["async-std1-rustls", "rustls-tls"]
|
||||
tokio1 = ["dep:tokio1_crate", "dep:async-trait", "dep:futures-io", "dep:futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "dep:tokio1_native_tls_crate"]
|
||||
tokio1-rustls = ["tokio1", "rustls", "dep:tokio1_rustls"]
|
||||
# deprecated
|
||||
tokio1-rustls-tls = ["tokio1-rustls", "rustls-tls"]
|
||||
tokio1-boring-tls = ["tokio1", "boring-tls", "dep:tokio1_boring"]
|
||||
|
||||
dkim = ["dep:base64", "dep:sha2", "dep:rsa", "dep:ed25519-dalek"]
|
||||
|
||||
# wasm support
|
||||
web = ["dep:web-time"]
|
||||
|
||||
[lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(lettre_ignore_tls_mismatch)'] }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs", "--cfg", "lettre_ignore_tls_mismatch"]
|
||||
|
||||
[[example]]
|
||||
name = "autoconfigure"
|
||||
required-features = ["smtp-transport", "native-tls"]
|
||||
|
||||
[[example]]
|
||||
name = "basic_html"
|
||||
required-features = ["file-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "maud_html"
|
||||
required-features = ["file-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_tls"
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_starttls"
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_selfsigned"
|
||||
required-features = ["smtp-transport", "native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_tls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2014 Alexis Mousset
|
||||
Copyright (c) 2014-2024 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2025 Paolo Barbolini <paolo@paolo565.org>
|
||||
Copyright (c) 2018 K. <kayo@illumium.org>
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
|
||||
137
README.md
137
README.md
@@ -1,27 +1,138 @@
|
||||
lettre [](https://travis-ci.org/lettre/lettre) [](https://coveralls.io/github/lettre/lettre?branch=master) [](https://crates.io/crates/lettre) [](./LICENSE)
|
||||
=========
|
||||
<h1 align="center">lettre</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
A mailer library for Rust
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
This is an email library written in Rust.
|
||||
See the [documentation](http://lettre.github.io/lettre) for more information.
|
||||
<br />
|
||||
|
||||
Install
|
||||
-------
|
||||
<div align="center">
|
||||
<a href="https://docs.rs/lettre">
|
||||
<img src="https://docs.rs/lettre/badge.svg"
|
||||
alt="docs" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/lettre">
|
||||
<img src="https://img.shields.io/crates/d/lettre.svg"
|
||||
alt="downloads" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://gitter.im/lettre/lettre">
|
||||
<img src="https://badges.gitter.im/lettre/lettre.svg"
|
||||
alt="chat on gitter" />
|
||||
</a>
|
||||
<a href="https://lettre.rs">
|
||||
<img src="https://img.shields.io/badge/visit-website-blueviolet"
|
||||
alt="website" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.11.16">
|
||||
<img src="https://deps.rs/crate/lettre/0.11.16/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
Lettre provides the following features:
|
||||
|
||||
* Multiple transport methods
|
||||
* Unicode support (for email content and addresses)
|
||||
* Secure delivery with SMTP using encryption and authentication
|
||||
* Easy email builders
|
||||
* Async support
|
||||
|
||||
Lettre does not provide (for now):
|
||||
|
||||
* Email parsing
|
||||
|
||||
## Supported Rust Versions
|
||||
|
||||
Lettre supports all Rust versions released in the last 6 months. At the time of writing
|
||||
the minimum supported Rust version is 1.74, but this could change at any time either from
|
||||
one of our dependencies bumping their MSRV or by a new patch release of lettre.
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.74 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.5"
|
||||
lettre = "0.11"
|
||||
```
|
||||
|
||||
Testing
|
||||
-------
|
||||
```rust,no_run
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
The tests require a mail server listening locally on port 25.
|
||||
fn main() {
|
||||
let email = Message::builder()
|
||||
.from(Mailbox::new("NoBody".to_owned(), "nobody@domain.tld".parse().unwrap()))
|
||||
.reply_to(Mailbox::new("Yuin".to_owned(), "yuin@domain.tld".parse().unwrap()))
|
||||
.to(Mailbox::new("Hei".to_owned(), "hei@domain.tld".parse().unwrap()))
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
License
|
||||
-------
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Not sure of which connect options to use?
|
||||
|
||||
Clone the lettre git repository and run the following command (replacing `SMTP_HOST` with your SMTP server's hostname)
|
||||
|
||||
```shell
|
||||
cargo run --example autoconfigure SMTP_HOST
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command. If you have python installed
|
||||
such a server can be launched with `python -m smtpd -n -c DebuggingServer 127.0.0.1:2525`
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
These are general steps to be followed when troubleshooting SMTP related issues.
|
||||
|
||||
- Ensure basic connectivity, ensure requisite ports are open and daemons are listening.
|
||||
- Confirm that your service provider allows traffic on the ports being used for mail transfer.
|
||||
- Check SMTP relay authentication and configuration.
|
||||
- Validate your DNS records. (DMARC, SPF, DKIM, MX)
|
||||
- Verify your SSL/TLS certificates are setup properly.
|
||||
- Investigate if filtering, formatting, or filesize limits are causing messages to be lost, delayed, or blocked by relays or remote hosts.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Anyone who interacts with Lettre in any space, including but not limited to
|
||||
this GitHub repository, must follow our [code of conduct](https://github.com/lettre/lettre/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## License
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
See LICENSE for details.
|
||||
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
|
||||
Kayo, under MIT license.
|
||||
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Report a security issue
|
||||
|
||||
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
||||
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
||||
should not be reported via the public GitHub Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
Security issues will be announced via the [RustSec advisory database](https://github.com/RustSec/advisory-db).
|
||||
27
benches/mailbox_parsing.rs
Normal file
27
benches/mailbox_parsing.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::message::{Mailbox, Mailboxes};
|
||||
|
||||
fn bench_parse_single(mailbox: &str) {
|
||||
assert!(mailbox.parse::<Mailbox>().is_ok());
|
||||
}
|
||||
|
||||
fn bench_parse_multiple(mailboxes: &str) {
|
||||
assert!(mailboxes.parse::<Mailboxes>().is_ok());
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("parse single mailbox", |b| {
|
||||
b.iter(|| bench_parse_single(black_box("\"Benchmark test\" <test@mail.local>")))
|
||||
});
|
||||
|
||||
c.bench_function("parse multiple mailboxes", |b| {
|
||||
b.iter(|| {
|
||||
bench_parse_multiple(black_box(
|
||||
"\"Benchmark test\" <test@mail.local>, Test <test@mail.local>, <test@mail.local>, test@mail.local",
|
||||
))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -1,44 +1,46 @@
|
||||
#![feature(test)]
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
extern crate lettre;
|
||||
extern crate test;
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
use lettre::transport::smtp::SmtpTransportBuilder;
|
||||
use lettre::transport::EmailTransport;
|
||||
use lettre::email::EmailBuilder;
|
||||
|
||||
#[bench]
|
||||
fn bench_simple_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525").unwrap().build();
|
||||
b.iter(|| {
|
||||
let email = EmailBuilder::new()
|
||||
.to("root@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello")
|
||||
.build()
|
||||
.unwrap();
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
c.bench_function("send email", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransportBuilder::new("127.0.0.1:2525")
|
||||
.unwrap()
|
||||
.connection_reuse(true)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = EmailBuilder::new()
|
||||
.to("root@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello")
|
||||
.build()
|
||||
.unwrap();
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
fn bench_reuse_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
c.bench_function("send email with connection reuse", move |b| {
|
||||
b.iter(|| {
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
let result = black_box(sender.send(&email));
|
||||
assert!(result.is_ok());
|
||||
})
|
||||
});
|
||||
sender.close()
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_simple_send, bench_reuse_send);
|
||||
criterion_main!(benches);
|
||||
|
||||
3
clippy.toml
Normal file
3
clippy.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
disallowed-methods = [
|
||||
{ "path" = "std::time::SystemTime::now", reason = "does not work on WASM environments", replacement = "crate::time::now" }
|
||||
]
|
||||
BIN
docs/lettre.png
Normal file
BIN
docs/lettre.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
24
examples/README.md
Normal file
24
examples/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Lettre Examples
|
||||
|
||||
This folder contains examples showing how to use lettre in your own projects.
|
||||
|
||||
## Message builder examples
|
||||
|
||||
- [basic_html.rs] - Create an HTML email.
|
||||
- [maud_html.rs] - Create an HTML email using a [maud](https://github.com/lambda-fairy/maud) template.
|
||||
|
||||
## SMTP Examples
|
||||
|
||||
- [smtp.rs] - Send an email using a local SMTP daemon on port 25 as a relay.
|
||||
- [smtp_tls.rs] - Send an email over SMTP encrypted with TLS and authenticating with username and password.
|
||||
- [smtp_starttls.rs] - Send an email over SMTP with STARTTLS and authenticating with username and password.
|
||||
- [smtp_selfsigned.rs] - Send an email over SMTP encrypted with TLS using a self-signed certificate and authenticating with username and password.
|
||||
- The [smtp_tls.rs] and [smtp_starttls.rs] examples also feature `async`hronous implementations powered by [Tokio](https://tokio.rs/).
|
||||
These files are prefixed with `tokio1_` or `asyncstd1_`.
|
||||
|
||||
[basic_html.rs]: ./basic_html.rs
|
||||
[maud_html.rs]: ./maud_html.rs
|
||||
[smtp.rs]: ./smtp.rs
|
||||
[smtp_tls.rs]: ./smtp_tls.rs
|
||||
[smtp_starttls.rs]: ./smtp_starttls.rs
|
||||
[smtp_selfsigned.rs]: ./smtp_selfsigned.rs
|
||||
33
examples/asyncstd1_smtp_starttls.rs
Normal file
33
examples/asyncstd1_smtp_starttls.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
33
examples/asyncstd1_smtp_tls.rs
Normal file
33
examples/asyncstd1_smtp_tls.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncStd1Executor, AsyncTransport, Message,
|
||||
};
|
||||
|
||||
#[async_std::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<AsyncStd1Executor> =
|
||||
AsyncSmtpTransport::<AsyncStd1Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
93
examples/autoconfigure.rs
Normal file
93
examples/autoconfigure.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use std::{env, process, time::Duration};
|
||||
|
||||
use lettre::SmtpTransport;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let smtp_host = match env::args().nth(1) {
|
||||
Some(smtp_host) => smtp_host,
|
||||
None => {
|
||||
println!("Please provide the SMTP host as the first argument to this command");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// TLS wrapped connection
|
||||
{
|
||||
tracing::info!(
|
||||
"Trying to establish a TLS wrapped connection to {}",
|
||||
smtp_host
|
||||
);
|
||||
|
||||
let transport = SmtpTransport::relay(&smtp_host)
|
||||
.expect("build SmtpTransport::relay")
|
||||
.timeout(Some(Duration::from_secs(10)))
|
||||
.build();
|
||||
match transport.test_connection() {
|
||||
Ok(true) => {
|
||||
tracing::info!("Successfully connected to {} via a TLS wrapped connection (SmtpTransport::relay). This is the fastest option available for connecting to an SMTP server", smtp_host);
|
||||
}
|
||||
Ok(false) => {
|
||||
tracing::error!("Couldn't connect to {} via a TLS wrapped connection. No more information is available", smtp_host);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(err = %err, "Couldn't connect to {} via a TLS wrapped connection", smtp_host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Plaintext connection which MUST then successfully upgrade to TLS via STARTTLS
|
||||
{
|
||||
tracing::info!("Trying to establish a plaintext connection to {} and then upgrading it via the SMTP STARTTLS extension", smtp_host);
|
||||
|
||||
let transport = SmtpTransport::starttls_relay(&smtp_host)
|
||||
.expect("build SmtpTransport::starttls_relay")
|
||||
.timeout(Some(Duration::from_secs(10)))
|
||||
.build();
|
||||
match transport.test_connection() {
|
||||
Ok(true) => {
|
||||
tracing::info!("Successfully connected to {} via a plaintext connection which then got upgraded to TLS via the SMTP STARTTLS extension (SmtpTransport::starttls_relay). This is the second best option after the previous TLS wrapped option", smtp_host);
|
||||
}
|
||||
Ok(false) => {
|
||||
tracing::error!(
|
||||
"Couldn't connect to {} via STARTTLS. No more information is available",
|
||||
smtp_host
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(err = %err, "Couldn't connect to {} via STARTTLS", smtp_host);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
|
||||
// Plaintext connection (very insecure)
|
||||
{
|
||||
tracing::info!(
|
||||
"Trying to establish a plaintext connection to {}",
|
||||
smtp_host
|
||||
);
|
||||
|
||||
let transport = SmtpTransport::builder_dangerous(&smtp_host)
|
||||
.timeout(Some(Duration::from_secs(10)))
|
||||
.build();
|
||||
match transport.test_connection() {
|
||||
Ok(true) => {
|
||||
tracing::info!("Successfully connected to {} via a plaintext connection. This option is very insecure and shouldn't be used on the public internet (SmtpTransport::builder_dangerous)", smtp_host);
|
||||
}
|
||||
Ok(false) => {
|
||||
tracing::error!(
|
||||
"Couldn't connect to {} via a plaintext connection. No more information is available",
|
||||
smtp_host
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(err = %err, "Couldn't connect to {} via a plaintext connection", smtp_host);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
49
examples/basic_html.rs
Normal file
49
examples/basic_html.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
use lettre::{
|
||||
message::{header, MultiPart, SinglePart},
|
||||
FileTransport, Message, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
// The html we want to send.
|
||||
let html = r#"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Hello from Lettre!</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: flex; flex-direction: column; align-items: center;">
|
||||
<h2 style="font-family: Arial, Helvetica, sans-serif;">Hello from Lettre!</h2>
|
||||
<h4 style="font-family: Arial, Helvetica, sans-serif;">A mailer library for Rust</h4>
|
||||
</div>
|
||||
</body>
|
||||
</html>"#;
|
||||
|
||||
// Build the message.
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Hello from Lettre!")
|
||||
.multipart(
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(html)),
|
||||
),
|
||||
)
|
||||
.expect("failed to build email");
|
||||
|
||||
// Create our mailer. Please see the other examples for creating SMTP mailers.
|
||||
// The path given here must exist on the filesystem.
|
||||
let mailer = FileTransport::new("./");
|
||||
|
||||
// Store the message when you're ready.
|
||||
mailer.send(&email).expect("failed to deliver message");
|
||||
}
|
||||
58
examples/maud_html.rs
Normal file
58
examples/maud_html.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
use lettre::{
|
||||
message::{header, MultiPart, SinglePart},
|
||||
FileTransport, Message, Transport,
|
||||
};
|
||||
use maud::html;
|
||||
|
||||
fn main() {
|
||||
// The recipient's name. We might obtain this from a form or their email address.
|
||||
let recipient = "Hei";
|
||||
|
||||
// Create the html we want to send.
|
||||
let html = html! {
|
||||
head {
|
||||
title { "Hello from Lettre!" }
|
||||
style type="text/css" {
|
||||
"h2, h4 { font-family: Arial, Helvetica, sans-serif; }"
|
||||
}
|
||||
}
|
||||
div style="display: flex; flex-direction: column; align-items: center;" {
|
||||
h2 { "Hello from Lettre!" }
|
||||
// Substitute in the name of our recipient.
|
||||
p { "Dear " (recipient) "," }
|
||||
p { "This email was sent with Lettre, a mailer library for Rust!"}
|
||||
p {
|
||||
"This example uses "
|
||||
a href="https://crates.io/crates/maud" { "maud" }
|
||||
". It is about 20% cooler than the basic HTML example."
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Build the message.
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Hello from Lettre!")
|
||||
.multipart(
|
||||
MultiPart::alternative() // This is composed of two parts.
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Hello from Lettre! A mailer library for Rust")), // Every message should have a plain text fallback.
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(html.into_string()),
|
||||
),
|
||||
)
|
||||
.expect("failed to build email");
|
||||
|
||||
// Create our mailer. Please see the other examples for creating SMTP mailers.
|
||||
// The path given here must exist on the filesystem.
|
||||
let mailer = FileTransport::new("./");
|
||||
|
||||
// Store the message when you're ready.
|
||||
mailer.send(&email).expect("failed to deliver message");
|
||||
}
|
||||
23
examples/smtp.rs
Normal file
23
examples/smtp.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mailer = SmtpTransport::unencrypted_localhost();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
46
examples/smtp_selfsigned.rs
Normal file
46
examples/smtp_selfsigned.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use std::fs;
|
||||
|
||||
use lettre::{
|
||||
message::header::ContentType,
|
||||
transport::smtp::{
|
||||
authentication::Credentials,
|
||||
client::{Certificate, Tls, TlsParameters},
|
||||
},
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
// Use a custom certificate stored on disk to securely verify the server's certificate
|
||||
let pem_cert = fs::read("certificate.pem").unwrap();
|
||||
let cert = Certificate::from_pem(&pem_cert).unwrap();
|
||||
let tls = TlsParameters::builder("smtp.server.com".to_owned())
|
||||
.add_root_certificate(cert)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to the smtp server
|
||||
let mailer = SmtpTransport::builder_dangerous("smtp.server.com")
|
||||
.port(465)
|
||||
.tls(Tls::Wrapper(tls))
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
31
examples/smtp_starttls.rs
Normal file
31
examples/smtp_starttls.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
SmtpTransport, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = SmtpTransport::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
31
examples/smtp_tls.rs
Normal file
31
examples/smtp_tls.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
SmtpTransport, Transport,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = SmtpTransport::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
37
examples/tokio1_smtp_starttls.rs
Normal file
37
examples/tokio1_smtp_starttls.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::starttls_relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
37
examples/tokio1_smtp_tls.rs
Normal file
37
examples/tokio1_smtp_tls.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
// This line is only to make it compile from lettre's examples folder,
|
||||
// since it uses Rust 2018 crate renaming to import tokio.
|
||||
// Won't be needed in user's code.
|
||||
use lettre::{
|
||||
message::header::ContentType, transport::smtp::authentication::Credentials, AsyncSmtpTransport,
|
||||
AsyncTransport, Message, Tokio1Executor,
|
||||
};
|
||||
use tokio1_crate as tokio;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new async year")
|
||||
.header(ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
AsyncSmtpTransport::<Tokio1Executor>::relay("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
match mailer.send(email).await {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
reorder_imports = true
|
||||
format_code_in_doc_comments = true
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "StdExternalCrate"
|
||||
|
||||
203
src/address/envelope.rs
Normal file
203
src/address/envelope.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
use super::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::message::header::{self, Headers};
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use crate::Error;
|
||||
|
||||
/// Simple email envelope representation
|
||||
///
|
||||
/// We only accept mailboxes, and do not support source routes (as per RFC).
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipient's addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
serde(deserialize_with = "serde_forward_path::deserialize")
|
||||
)]
|
||||
forward_path: Vec<Address>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<Address>,
|
||||
}
|
||||
|
||||
/// just like the default implementation to deserialize `Vec<Address>` but it
|
||||
/// forbids **de**serializing empty lists
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_forward_path {
|
||||
use super::Address;
|
||||
/// dummy type required for serde
|
||||
/// see example: <https://serde.rs/deserialize-map.html>
|
||||
struct CustomVisitor;
|
||||
impl<'de> serde::de::Visitor<'de> for CustomVisitor {
|
||||
type Value = Vec<Address>;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
formatter.write_str("a non-empty list of recipient addresses")
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: serde::de::SeqAccess<'de>,
|
||||
{
|
||||
let mut seq: Vec<Address> = Vec::with_capacity(access.size_hint().unwrap_or(0));
|
||||
while let Some(key) = access.next_element()? {
|
||||
seq.push(key);
|
||||
}
|
||||
if seq.is_empty() {
|
||||
Err(serde::de::Error::invalid_length(seq.len(), &self))
|
||||
} else {
|
||||
Ok(seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_seq(CustomVisitor {})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn deserializing_empty_recipient_list_returns_error() {
|
||||
assert!(
|
||||
serde_json::from_str::<crate::address::Envelope>(r#"{"forward_path": []}"#)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn deserializing_non_empty_recipient_list_is_ok() {
|
||||
serde_json::from_str::<crate::address::Envelope>(
|
||||
r#"{ "forward_path": [ {"user":"foo", "domain":"example.com"} ] }"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "sender@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `to` has no elements in it.
|
||||
pub fn new(from: Option<Address>, to: Vec<Address>) -> Result<Envelope, Error> {
|
||||
if to.is_empty() {
|
||||
return Err(Error::MissingTo);
|
||||
}
|
||||
Ok(Envelope {
|
||||
forward_path: to,
|
||||
reverse_path: from,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the destination addresses of the envelope.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert_eq!(envelope.to(), recipients.as_slice());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn to(&self) -> &[Address] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Gets the sender of the envelope.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use lettre::address::{Address, Envelope};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = "from@email.com".parse::<Address>()?;
|
||||
/// let recipients = vec!["to@email.com".parse::<Address>()?];
|
||||
///
|
||||
/// let envelope = Envelope::new(Some(sender), recipients.clone())?;
|
||||
/// assert!(envelope.from().is_some());
|
||||
///
|
||||
/// let senderless = Envelope::new(None, recipients.clone())?;
|
||||
/// assert!(senderless.from().is_none());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn from(&self) -> Option<&Address> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if any of the addresses in the envelope contains non-ascii chars
|
||||
pub(crate) fn has_non_ascii_addresses(&self) -> bool {
|
||||
self.reverse_path
|
||||
.iter()
|
||||
.chain(self.forward_path.iter())
|
||||
.any(|a| !a.is_ascii())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
impl TryFrom<&Headers> for Envelope {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(headers: &Headers) -> Result<Self, Self::Error> {
|
||||
let from = match headers.get::<header::Sender>() {
|
||||
// If there is a Sender, use it
|
||||
Some(sender) => Some(Mailbox::from(sender).email),
|
||||
// ... else try From
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(a)) => {
|
||||
let mut from: Vec<Mailbox> = a.into();
|
||||
if from.len() > 1 {
|
||||
return Err(Error::TooManyFrom);
|
||||
}
|
||||
let from = from.pop().expect("From header has 1 Mailbox");
|
||||
Some(from.email)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
addresses.extend(mailboxes.into_iter().map(|mb| mb.email));
|
||||
}
|
||||
}
|
||||
let mut to = vec![];
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::To>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Cc>().map(|h| h.0));
|
||||
add_addresses_from_mailboxes(&mut to, headers.get::<header::Bcc>().map(|h| h.0));
|
||||
|
||||
Self::new(from, to)
|
||||
}
|
||||
}
|
||||
12
src/address/mod.rs
Normal file
12
src/address/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
//! Email addresses
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
|
||||
mod envelope;
|
||||
mod types;
|
||||
|
||||
pub use self::{
|
||||
envelope::Envelope,
|
||||
types::{Address, AddressError},
|
||||
};
|
||||
112
src/address/serde.rs
Normal file
112
src/address/serde.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
use super::Address;
|
||||
|
||||
impl Serialize for Address {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Address {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
User,
|
||||
Domain,
|
||||
}
|
||||
|
||||
const FIELDS: &[&str] = &["user", "domain"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("'user' or 'domain'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"user" => Ok(Field::User),
|
||||
"domain" => Ok(Field::Domain),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddressVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for AddressVisitor {
|
||||
type Value = Address;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("email address string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut user = None;
|
||||
let mut domain = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::User => {
|
||||
if user.is_some() {
|
||||
return Err(DeError::duplicate_field("user"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_user(val).map_err(DeError::custom)?;
|
||||
user = Some(val);
|
||||
}
|
||||
Field::Domain => {
|
||||
if domain.is_some() {
|
||||
return Err(DeError::duplicate_field("domain"));
|
||||
}
|
||||
let val = map.next_value()?;
|
||||
Address::check_domain(val).map_err(DeError::custom)?;
|
||||
domain = Some(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
let user: &str = user.ok_or_else(|| DeError::missing_field("user"))?;
|
||||
let domain: &str = domain.ok_or_else(|| DeError::missing_field("domain"))?;
|
||||
Ok(Address::new(user, domain).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(AddressVisitor)
|
||||
}
|
||||
}
|
||||
306
src/address/types.rs
Normal file
306
src/address/types.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Representation of an email address
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
net::IpAddr,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use email_address::EmailAddress;
|
||||
use idna::domain_to_ascii;
|
||||
|
||||
/// Represents an email address with a user and a domain name.
|
||||
///
|
||||
/// This type contains email in canonical form (_user@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create an `Address` from a user and a domain:
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can also create an `Address` from a string literal by parsing it:
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Address {
|
||||
/// Complete address
|
||||
serialized: String,
|
||||
/// Index into `serialized` before the '@'
|
||||
at_start: usize,
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Creates a new email address from a user and domain.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// let expected = "user@email.com".parse::<Address>()?;
|
||||
/// assert_eq!(expected, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new<U: AsRef<str>, D: AsRef<str>>(user: U, domain: D) -> Result<Self, AddressError> {
|
||||
(user, domain).try_into()
|
||||
}
|
||||
|
||||
/// Gets the user portion of the `Address`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.user(), "user");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn user(&self) -> &str {
|
||||
&self.serialized[..self.at_start]
|
||||
}
|
||||
|
||||
/// Gets the domain portion of the `Address`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::Address;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("user", "email.com")?;
|
||||
/// assert_eq!(address.domain(), "email.com");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.serialized[self.at_start + 1..]
|
||||
}
|
||||
|
||||
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if EmailAddress::is_valid_local_part(user) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(AddressError::InvalidUser)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_domain(domain: &str) -> Result<(), AddressError> {
|
||||
Address::check_domain_ascii(domain).or_else(|_| {
|
||||
domain_to_ascii(domain)
|
||||
.map_err(|_| AddressError::InvalidDomain)
|
||||
.and_then(|domain| Address::check_domain_ascii(&domain))
|
||||
})
|
||||
}
|
||||
|
||||
fn check_domain_ascii(domain: &str) -> Result<(), AddressError> {
|
||||
// Domain
|
||||
if EmailAddress::is_valid_domain(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// IP
|
||||
let ip = domain
|
||||
.strip_prefix('[')
|
||||
.and_then(|ip| ip.strip_suffix(']'))
|
||||
.unwrap_or(domain);
|
||||
|
||||
if ip.parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
/// Check if the address contains non-ascii chars
|
||||
pub(super) fn is_ascii(&self) -> bool {
|
||||
self.serialized.is_ascii()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
f.write_str(&self.serialized)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Address {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, AddressError> {
|
||||
let at_start = check_address(val)?;
|
||||
Ok(Address {
|
||||
serialized: val.into(),
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<U, D> TryFrom<(U, D)> for Address
|
||||
where
|
||||
U: AsRef<str>,
|
||||
D: AsRef<str>,
|
||||
{
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from((user, domain): (U, D)) -> Result<Self, Self::Error> {
|
||||
let user = user.as_ref();
|
||||
Address::check_user(user)?;
|
||||
|
||||
let domain = domain.as_ref();
|
||||
Address::check_domain(domain)?;
|
||||
|
||||
let serialized = format!("{user}@{domain}");
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for Address {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(serialized: String) -> Result<Self, AddressError> {
|
||||
let at_start = check_address(&serialized)?;
|
||||
Ok(Address {
|
||||
serialized,
|
||||
at_start,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Address {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.serialized
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for Address {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
self.serialized.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
fn check_address(val: &str) -> Result<usize, AddressError> {
|
||||
let mut parts = val.rsplitn(2, '@');
|
||||
let domain = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
let user = parts.next().ok_or(AddressError::MissingParts)?;
|
||||
|
||||
Address::check_user(user)?;
|
||||
Address::check_domain(domain)?;
|
||||
Ok(user.len())
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
#[non_exhaustive]
|
||||
/// Errors in email addresses parsing
|
||||
pub enum AddressError {
|
||||
/// Missing domain or user
|
||||
MissingParts,
|
||||
/// Unbalanced angle bracket
|
||||
Unbalanced,
|
||||
/// Invalid email user
|
||||
InvalidUser,
|
||||
/// Invalid email domain
|
||||
InvalidDomain,
|
||||
/// Invalid input found
|
||||
InvalidInput,
|
||||
}
|
||||
|
||||
impl Error for AddressError {}
|
||||
|
||||
impl Display for AddressError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
match self {
|
||||
AddressError::MissingParts => f.write_str("Missing domain or user"),
|
||||
AddressError::Unbalanced => f.write_str("Unbalanced angle bracket"),
|
||||
AddressError::InvalidUser => f.write_str("Invalid email user"),
|
||||
AddressError::InvalidDomain => f.write_str("Invalid email domain"),
|
||||
AddressError::InvalidInput => f.write_str("Invalid input"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ascii_address() {
|
||||
let addr_str = "something@example.com";
|
||||
let addr = Address::from_str(addr_str).unwrap();
|
||||
let addr2 = Address::new("something", "example.com").unwrap();
|
||||
assert_eq!(addr, addr2);
|
||||
assert_eq!(addr.user(), "something");
|
||||
assert_eq!(addr.domain(), "example.com");
|
||||
assert_eq!(addr2.user(), "something");
|
||||
assert_eq!(addr2.domain(), "example.com");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_address_ipv4() {
|
||||
let addr_str = "something@1.1.1.1";
|
||||
let addr = Address::from_str(addr_str).unwrap();
|
||||
let addr2 = Address::new("something", "1.1.1.1").unwrap();
|
||||
assert_eq!(addr, addr2);
|
||||
assert_eq!(addr.user(), "something");
|
||||
assert_eq!(addr.domain(), "1.1.1.1");
|
||||
assert_eq!(addr2.user(), "something");
|
||||
assert_eq!(addr2.domain(), "1.1.1.1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii_address_ipv6() {
|
||||
let addr_str = "something@[2606:4700:4700::1111]";
|
||||
let addr = Address::from_str(addr_str).unwrap();
|
||||
let addr2 = Address::new("something", "[2606:4700:4700::1111]").unwrap();
|
||||
assert_eq!(addr, addr2);
|
||||
assert_eq!(addr.user(), "something");
|
||||
assert_eq!(addr.domain(), "[2606:4700:4700::1111]");
|
||||
assert_eq!(addr2.user(), "something");
|
||||
assert_eq!(addr2.domain(), "[2606:4700:4700::1111]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_parts() {
|
||||
assert!(Address::check_user("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").is_err());
|
||||
assert!(
|
||||
Address::check_domain("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com").is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/base64.rs
Normal file
12
src/base64.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
use ::base64::{
|
||||
engine::{general_purpose::STANDARD, Engine},
|
||||
DecodeError,
|
||||
};
|
||||
|
||||
pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
|
||||
STANDARD.encode(input)
|
||||
}
|
||||
|
||||
pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
|
||||
STANDARD.decode(input)
|
||||
}
|
||||
354
src/email/mod.rs
354
src/email/mod.rs
@@ -1,354 +0,0 @@
|
||||
//! Simple email (very incomplete)
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use email_format::{Header, Mailbox, MimeMessage};
|
||||
use time::{Tm, now};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Address`
|
||||
pub trait ToHeader {
|
||||
/// Converts to a `Header` struct
|
||||
fn to_header(&self) -> Header;
|
||||
}
|
||||
|
||||
impl ToHeader for Header {
|
||||
fn to_header(&self) -> Header {
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToHeader for (&'a str, &'a str) {
|
||||
fn to_header(&self) -> Header {
|
||||
let (name, value) = *self;
|
||||
Header::new(name.to_string(), value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Mailbox`
|
||||
pub trait ToMailbox {
|
||||
/// Converts to a `Mailbox` struct
|
||||
fn to_mailbox(&self) -> Mailbox;
|
||||
}
|
||||
|
||||
impl ToMailbox for Mailbox {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToMailbox for &'a str {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
Mailbox::new(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToMailbox for (&'a str, &'a str) {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
let (address, alias) = *self;
|
||||
Mailbox::new_with_name(alias.to_string(), address.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
/// The enveloppe recipients addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
from: Option<String>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
/// The enveloppe recipients addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
from: String,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
|
||||
impl Display for Email {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.message.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
to: vec![],
|
||||
from: None,
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body(mut self, body: &str) -> EmailBuilder {
|
||||
self.message.body = body.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||
self.insert_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
fn insert_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.message.headers.insert(header.to_header());
|
||||
}
|
||||
|
||||
/// Adds a `From` header and store the sender address
|
||||
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("From", mailbox.to_string().as_ref()));
|
||||
self.from = Some(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and store the recipient address
|
||||
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("To", mailbox.to_string().as_ref()));
|
||||
self.to.push(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and store the recipient address
|
||||
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Cc", mailbox.to_string().as_ref()));
|
||||
self.to.push(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Reply-To", mailbox.to_string().as_ref()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Sender", mailbox.to_string().as_ref()));
|
||||
self.from = Some(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
||||
self.insert_header(("Subject", subject));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Email
|
||||
pub fn build(mut self) -> Result<Email, &'static str> {
|
||||
if self.from.is_none() {
|
||||
return Err("No from address");
|
||||
}
|
||||
if self.to.is_empty() {
|
||||
return Err("No to address");
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||
}
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
match Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id)) {
|
||||
Ok(header) => self.insert_header(header),
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
self.message.update_headers();
|
||||
|
||||
Ok(Email {
|
||||
message: self.message,
|
||||
to: self.to,
|
||||
from: self.from.unwrap(),
|
||||
message_id: message_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Email sendable by an SMTP client
|
||||
pub trait SendableEmail {
|
||||
/// From address
|
||||
fn from_address(&self) -> String;
|
||||
/// To addresses
|
||||
fn to_addresses(&self) -> Vec<String>;
|
||||
/// Message content
|
||||
fn message(&self) -> String;
|
||||
/// Message ID
|
||||
fn message_id(&self) -> String;
|
||||
}
|
||||
|
||||
/// Minimal email structure
|
||||
pub struct SimpleSendableEmail {
|
||||
/// From address
|
||||
from: String,
|
||||
/// To addresses
|
||||
to: Vec<String>,
|
||||
/// Message
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl SimpleSendableEmail {
|
||||
/// Returns a new email
|
||||
pub fn new(from_address: &str, to_address: Vec<String>, message: &str) -> SimpleSendableEmail {
|
||||
SimpleSendableEmail {
|
||||
from: from_address.to_string(),
|
||||
to: to_address,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SendableEmail for SimpleSendableEmail {
|
||||
fn from_address(&self) -> String {
|
||||
self.from.clone()
|
||||
}
|
||||
|
||||
fn to_addresses(&self) -> Vec<String> {
|
||||
self.to.clone()
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
self.message.clone()
|
||||
}
|
||||
|
||||
fn message_id(&self) -> String {
|
||||
format!("{}", Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl SendableEmail for Email {
|
||||
fn to_addresses(&self) -> Vec<String> {
|
||||
self.to.clone()
|
||||
}
|
||||
|
||||
fn from_address(&self) -> String {
|
||||
self.from.clone()
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
|
||||
fn message_id(&self) -> String {
|
||||
format!("{}", self.message_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use time::now;
|
||||
|
||||
use uuid::Uuid;
|
||||
use email_format::{Header, MimeMessage};
|
||||
|
||||
use super::{Email, EmailBuilder, SendableEmail};
|
||||
|
||||
#[test]
|
||||
fn test_email_display() {
|
||||
let current_message = Uuid::new_v4();
|
||||
|
||||
let mut email = Email {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
to: vec![],
|
||||
from: "".to_string(),
|
||||
message_id: current_message,
|
||||
};
|
||||
|
||||
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}@rust-smtp>",
|
||||
current_message))
|
||||
.unwrap());
|
||||
|
||||
email.message
|
||||
.headers
|
||||
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
|
||||
.unwrap());
|
||||
|
||||
email.message.body = "body".to_string();
|
||||
|
||||
assert_eq!(format!("{}", email),
|
||||
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
|
||||
current_message));
|
||||
assert_eq!(current_message.to_string(), email.message_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(format!("{}", email),
|
||||
format!("To: <user@localhost>\r\nFrom: <user@localhost>\r\nCc: \"Alias\" \
|
||||
<cc@localhost>\r\nReply-To: <reply@localhost>\r\nSender: \
|
||||
<sender@localhost>\r\nDate: {}\r\nSubject: Hello\r\nX-test: \
|
||||
value\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
email.message_id()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(email.from_address(), "sender@localhost".to_string());
|
||||
assert_eq!(email.to_addresses(),
|
||||
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
|
||||
assert_eq!(email.message(), format!("{}", email));
|
||||
}
|
||||
|
||||
}
|
||||
54
src/error.rs
Normal file
54
src/error.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! Error type for email messages
|
||||
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
// FIXME message-specific errors
|
||||
/// Error type for email content
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
MissingTo,
|
||||
/// Can only be one from in envelope
|
||||
TooManyFrom,
|
||||
/// Invalid email: missing at
|
||||
EmailMissingAt,
|
||||
/// Invalid email: missing local part
|
||||
EmailMissingLocalPart,
|
||||
/// Invalid email: missing domain
|
||||
EmailMissingDomain,
|
||||
/// Cannot parse filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(std::io::Error),
|
||||
/// Non-ASCII chars
|
||||
NonAsciiChars,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Error::MissingFrom => f.write_str("missing source address, invalid envelope"),
|
||||
Error::MissingTo => f.write_str("missing destination address, invalid envelope"),
|
||||
Error::TooManyFrom => f.write_str("there can only be one source address"),
|
||||
Error::EmailMissingAt => f.write_str("missing @ in email address"),
|
||||
Error::EmailMissingLocalPart => f.write_str("missing local part in email address"),
|
||||
Error::EmailMissingDomain => f.write_str("missing domain in email address"),
|
||||
Error::CannotParseFilename => f.write_str("could not parse attachment filename"),
|
||||
Error::NonAsciiChars => f.write_str("contains non-ASCII chars"),
|
||||
Error::Io(e) => e.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
298
src/executor.rs
Normal file
298
src/executor.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use std::fmt::Debug;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::future::Future;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::io::Result as IoResult;
|
||||
#[cfg(feature = "file-transport")]
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
use futures_util::future::BoxFuture;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::AsyncSmtpConnection;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::client::Tls;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::extension::ClientId;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// Async executor abstraction trait
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait Executor: Debug + Send + Sync + 'static + private::Sealed {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[allow(private_bounds)]
|
||||
type Handle: SpawnHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep: Future<Output = ()> + Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()>;
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[async_trait]
|
||||
pub(crate) trait SpawnHandle: Debug + Send + Sync + 'static + private::Sealed {
|
||||
async fn shutdown(&self);
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `tokio` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct Tokio1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Executor for Tokio1Executor {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = tokio1_crate::task::JoinHandle<()>;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = tokio1_crate::time::Sleep;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
tokio1_crate::spawn(fut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
tokio1_crate::time::sleep(duration)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn = AsyncSmtpConnection::connect_tokio1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
tokio1_crate::fs::read(path).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
tokio1_crate::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for tokio1_crate::task::JoinHandle<()> {
|
||||
async fn shutdown(&self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/// Async [`Executor`] using `async-std` `1.x`
|
||||
///
|
||||
/// Used by [`AsyncSmtpTransport`], [`AsyncSendmailTransport`] and [`AsyncFileTransport`]
|
||||
/// in order to be able to work with different async runtimes.
|
||||
///
|
||||
/// [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
/// [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
/// [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
#[derive(Debug)]
|
||||
pub struct AsyncStd1Executor;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Executor for AsyncStd1Executor {
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Handle = futures_util::future::AbortHandle;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
type Sleep = BoxFuture<'static, ()>;
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn spawn<F>(fut: F) -> Self::Handle
|
||||
where
|
||||
F: Future<Output = ()> + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
let (handle, registration) = futures_util::future::AbortHandle::new_pair();
|
||||
async_std::task::spawn(futures_util::future::Abortable::new(fut, registration));
|
||||
handle
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
fn sleep(duration: Duration) -> Self::Sleep {
|
||||
let fut = async_std::task::sleep(duration);
|
||||
Box::pin(fut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn = AsyncSmtpConnection::connect_asyncstd1(
|
||||
(hostname, port),
|
||||
timeout,
|
||||
hello_name,
|
||||
tls_parameters,
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
match tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
async fn fs_read(path: &Path) -> IoResult<Vec<u8>> {
|
||||
async_std::fs::read(path).await
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
async fn fs_write(path: &Path, contents: &[u8]) -> IoResult<()> {
|
||||
async_std::fs::write(path, contents).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl SpawnHandle for futures_util::future::AbortHandle {
|
||||
async fn shutdown(&self) {
|
||||
self.abort();
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Sealed for super::Tokio1Executor {}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Sealed for super::AsyncStd1Executor {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
impl Sealed for tokio1_crate::task::JoinHandle<()> {}
|
||||
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
impl Sealed for futures_util::future::AbortHandle {}
|
||||
}
|
||||
505
src/lib.rs
505
src/lib.rs
@@ -1,209 +1,384 @@
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//! Lettre is an email library that allows creating and sending messages. It provides:
|
||||
//!
|
||||
//! ## Architecture
|
||||
//! * An easy to use email builder
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! This mailer is divided into:
|
||||
//! Lettre requires Rust 1.74 or newer.
|
||||
//!
|
||||
//! * An `email` part: builds the email message
|
||||
//! * A `transport` part: contains the available transports for your emails. To be sendable, the
|
||||
//! emails have to implement `SendableEmail`.
|
||||
//! ## Features
|
||||
//!
|
||||
//! ## SMTP transport
|
||||
//! This section lists each lettre feature and briefly explains it.
|
||||
//! More info about each module can be found in the corresponding module page.
|
||||
//!
|
||||
//! This SMTP follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), but is still
|
||||
//! a work in progress. It is designed to efficiently send emails from an
|
||||
//! application to a
|
||||
//! relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC
|
||||
//! compliance checks.
|
||||
//! Features with `📫` near them are enabled by default.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//! ### Typed message builder
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and
|
||||
//! CRAM-MD5 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//! _Strongly typed [`message`] builder_
|
||||
//!
|
||||
//! ### Simple example
|
||||
//! * **builder** 📫: Enable the [`Message`] builder
|
||||
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//! ### SMTP transport
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::transport::smtp::{SmtpTransport, SmtpTransportBuilder};
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! _Send emails using [`SMTP`]_
|
||||
//!
|
||||
//! // Create an email
|
||||
//! let email = EmailBuilder::new()
|
||||
//! // Addresses can be specified by the couple (email, alias)
|
||||
//! .to(("user@example.org", "Firstname Lastname"))
|
||||
//! // ... or by an address only
|
||||
//! .from("user@example.com")
|
||||
//! .subject("Hi, Hello world")
|
||||
//! .body("Hello world.")
|
||||
//! .build().unwrap();
|
||||
//! * **smtp-transport** 📫: Enable the SMTP transport
|
||||
//! * **pool** 📫: Connection pool for SMTP transport
|
||||
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
|
||||
//!
|
||||
//! // Open a local connection on port 25
|
||||
//! let mut mailer =
|
||||
//! SmtpTransportBuilder::localhost().unwrap().build();
|
||||
//! // Send the email
|
||||
//! let result = mailer.send(email);
|
||||
//! #### SMTP over TLS via the native-tls crate
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! ### Complete example
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
|
||||
//! on all other platforms.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! use lettre::transport::smtp::{SecurityLevel, SmtpTransport,
|
||||
//! SmtpTransportBuilder};
|
||||
//! use lettre::transport::smtp::authentication::Mechanism;
|
||||
//! use lettre::transport::smtp::SUBMISSION_PORT;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! let mut builder = EmailBuilder::new();
|
||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||
//! builder = builder.cc(("user@example.net", "Alias name"));
|
||||
//! builder = builder.from("no-reply@example.com");
|
||||
//! builder = builder.from("no-reply@example.eu");
|
||||
//! builder = builder.sender("no-reply@example.com");
|
||||
//! builder = builder.subject("Hello world");
|
||||
//! builder = builder.body("Hi, Hello world.");
|
||||
//! builder = builder.reply_to("contact@example.com");
|
||||
//! builder = builder.add_header(("X-Custom-Header", "my header"));
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! let email = builder.build().unwrap();
|
||||
//! ##### Building lettre with OpenSSL
|
||||
//!
|
||||
//! // Connect to a remote server on a custom port
|
||||
//! let mut mailer = SmtpTransportBuilder::new(("server.tld",
|
||||
//! SUBMISSION_PORT)).unwrap()
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name("my.hostname.tld")
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials("username", "password")
|
||||
//! // Specify a TLS security level. You can also specify an SslContext with
|
||||
//! // .ssl_context(SslContext::Ssl23)
|
||||
//! .security_level(SecurityLevel::AlwaysEncrypt)
|
||||
//! // Enable SMTPUTF8 is the server supports it
|
||||
//! .smtp_utf8(true)
|
||||
//! // Configure accepted authetication mechanisms
|
||||
//! .authentication_mechanisms(vec![Mechanism::CramMd5])
|
||||
//! // Enable connection reuse
|
||||
//! .connection_reuse(true).build();
|
||||
//! When building lettre with native-tls on a system that makes
|
||||
//! use of OpenSSL, the following packages will need to be installed
|
||||
//! in order for the build and the compiled program to run properly.
|
||||
//!
|
||||
//! let result_1 = mailer.send(email.clone());
|
||||
//! assert!(result_1.is_ok());
|
||||
//! | Distro | Build-time packages | Runtime packages |
|
||||
//! | ------------ | -------------------------- | ---------------------------- |
|
||||
//! | Debian | `pkg-config`, `libssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//! | Alpine Linux | `pkgconf`, `openssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//!
|
||||
//! // The second email will use the same connection
|
||||
//! let result_2 = mailer.send(email);
|
||||
//! assert!(result_2.is_ok());
|
||||
//! #### SMTP over TLS via the boring crate (Boring TLS)
|
||||
//!
|
||||
//! // Explicitely close the SMTP transaction as we enabled connection reuse
|
||||
//! mailer.close();
|
||||
//! ```
|
||||
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
|
||||
//!
|
||||
//! ### Lower level
|
||||
//! * **boring-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! You can also send commands, here is a simple email transaction without
|
||||
//! error handling:
|
||||
//! NOTE: boring-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::transport::smtp::SMTP_PORT;
|
||||
//! use lettre::transport::smtp::client::Client;
|
||||
//! use lettre::transport::smtp::client::net::NetworkStream;
|
||||
//! #### SMTP over TLS via the rustls crate
|
||||
//!
|
||||
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||
//! let _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
//! let _ = email_client.ehlo("my_hostname");
|
||||
//! let _ = email_client.mail("user@example.com", None);
|
||||
//! let _ = email_client.rcpt("user@example.org");
|
||||
//! let _ = email_client.data();
|
||||
//! let _ = email_client.message("Test email");
|
||||
//! let _ = email_client.quit();
|
||||
//! ```
|
||||
//! _Secure SMTP connections using TLS from the `rustls` crate_
|
||||
//!
|
||||
//! ## Stub transport
|
||||
//! * **rustls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-rustls**: TLS support for the `tokio1` async version of the API
|
||||
//! * **async-std1-rustls**: TLS support for the `async-std1` async version of the API
|
||||
//!
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//! ##### rustls crypto backends
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::transport::stub::StubEmailTransport;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::email::EmailBuilder;
|
||||
//! _The crypto implementation to use with rustls_
|
||||
//!
|
||||
//! let mut sender = StubEmailTransport;
|
||||
//! let email = EmailBuilder::new()
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! let result = sender.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//! When the `rustls` feature is enabled, one of the following crypto backends MUST also
|
||||
//! be enabled.
|
||||
//!
|
||||
//! Will log the line:
|
||||
//! * **aws-lc-rs**: use [AWS-LC] (via [`aws-lc-rs`]) as the `rustls` crypto backend
|
||||
//! * **ring**: use [`ring`] as the `rustls` crypto backend
|
||||
//!
|
||||
//! ```text
|
||||
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
|
||||
//! ```
|
||||
//! When enabling `aws-lc-rs`, the `fips` feature can also be enabled to have
|
||||
//! rustls use the FIPS certified module of AWS-LC.
|
||||
//!
|
||||
//! ## File transport
|
||||
//! `aws-lc-rs` may require cmake on some platforms to compile.
|
||||
//! `fips` always requires cmake and the Go compiler to compile.
|
||||
//!
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.txt`.
|
||||
//! It can be useful for testing purposes.
|
||||
//! ##### rustls certificate verification backend
|
||||
//!
|
||||
//! ```rust
|
||||
//! use std::env::temp_dir;
|
||||
//! _The TLS certificate verification backend to use with rustls_
|
||||
//!
|
||||
//! use lettre::transport::file::FileEmailTransport;
|
||||
//! use lettre::transport::EmailTransport;
|
||||
//! use lettre::email::{EmailBuilder, SendableEmail};
|
||||
//! When the `rustls` feature is enabled, one of the following verification backends
|
||||
//! MUST also be enabled.
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let mut sender = FileEmailTransport::new(temp_dir());
|
||||
//! let email = EmailBuilder::new()
|
||||
//! .to("root@localhost")
|
||||
//! .from("user@localhost")
|
||||
//! .body("Hello World!")
|
||||
//! .subject("Hello")
|
||||
//! .build()
|
||||
//! .unwrap();
|
||||
//! * **rustls-platform-verifier**: verify TLS certificate using the OS's native certificate store (see [`rustls-platform-verifier`])
|
||||
//! * **rustls-native-certs**: verify TLS certificates using the platform's native certificate store (see [`rustls-native-certs`]) - when in doubt use `rustls-platform-verifier`
|
||||
//! * **webpki-roots**: verify TLS certificates against Mozilla's root certificates (see [`webpki-roots`])
|
||||
//!
|
||||
//! let result = sender.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//! Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
|
||||
//! The following packages will need to be installed in order for the build
|
||||
//! stage and the compiled program to run properly.
|
||||
//!
|
||||
//! ```text
|
||||
//! b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
|
||||
//! To: <root@localhost>
|
||||
//! From: <user@localhost>
|
||||
//! Subject: Hello
|
||||
//! Date: Sat, 31 Oct 2015 13:42:19 +0100
|
||||
//! Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
|
||||
//! | Verification backend | Distro | Build-time packages | Runtime packages |
|
||||
//! | --------------------- | ------------ | -------------------------- | ---------------------------- |
|
||||
//! | `rustls-platform-verifier` | Debian | none | `ca-certificates` |
|
||||
//! | `rustls-platform-verifier` | Alpine Linux | none | `ca-certificates` |
|
||||
//! | `rustls-native-certs` | Debian | none | `ca-certificates` |
|
||||
//! | `rustls-native-certs` | Alpine Linux | none | `ca-certificates` |
|
||||
//! | `webpki-roots` | any | none | none |
|
||||
//!
|
||||
//! Hello World!
|
||||
//! ```
|
||||
//! ### Sendmail transport
|
||||
//!
|
||||
//! _Send emails using the [`sendmail`] command_
|
||||
//!
|
||||
//! * **sendmail-transport**: Enable the `sendmail` transport
|
||||
//!
|
||||
//! ### File transport
|
||||
//!
|
||||
//! _Save emails as an `.eml` [`file`]_
|
||||
//!
|
||||
//! * **file-transport**: Enable the file transport (saves emails into an `.eml` file)
|
||||
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file (additionally saves envelopes into a `.json` file)
|
||||
//!
|
||||
//! ### Async execution runtimes
|
||||
//!
|
||||
//! _Use [tokio] or [async-std] as an async execution runtime for sending emails_
|
||||
//!
|
||||
//! The correct runtime version must be chosen in order for lettre to work correctly.
|
||||
//! For example, when sending emails from a Tokio 1.x context, the Tokio 1.x executor
|
||||
//! ([`Tokio1Executor`]) must be used. Using a different version (for example Tokio 0.2.x),
|
||||
//! or async-std, would result in a runtime panic.
|
||||
//!
|
||||
//! * **tokio1**: Allow to asynchronously send emails using [Tokio 1.x]
|
||||
//! * **async-std1**: Allow to asynchronously send emails using [async-std 1.x]
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ### Misc features
|
||||
//!
|
||||
//! _Additional features_
|
||||
//!
|
||||
//! * **serde**: Serialization/Deserialization of entities
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **mime03**: Allow creating a [`ContentType`] from an existing [mime 0.3] `Mime` struct
|
||||
//! * **dkim**: Add support for signing email with DKIM
|
||||
//! * **web**: WebAssembly support using the `web-time` crate for time operations
|
||||
//!
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`sendmail`]: crate::transport::sendmail
|
||||
//! [`file`]: crate::transport::file
|
||||
//! [`ContentType`]: crate::message::header::ContentType
|
||||
//! [tokio]: https://docs.rs/tokio/1
|
||||
//! [async-std]: https://docs.rs/async-std/1
|
||||
//! [AWS-LC]: https://github.com/aws/aws-lc
|
||||
//! [`aws-lc-rs`]: https://crates.io/crates/aws-lc-rs
|
||||
//! [`ring`]: https://crates.io/crates/ring
|
||||
//! [`rustls-platform-verifier`]: https://crates.io/crates/rustls-platform-verifier
|
||||
//! [`rustls-native-certs`]: https://crates.io/crates/rustls-native-certs
|
||||
//! [`webpki-roots`]: https://crates.io/crates/webpki-roots
|
||||
//! [Tokio 1.x]: https://docs.rs/tokio/1
|
||||
//! [async-std 1.x]: https://docs.rs/async-std/1
|
||||
//! [mime 0.3]: https://docs.rs/mime/0.3
|
||||
//! [DKIM]: https://datatracker.ietf.org/doc/html/rfc6376
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.11.16")]
|
||||
#![doc(html_favicon_url = "https://lettre.rs/favicon.ico")]
|
||||
#![doc(html_logo_url = "https://avatars0.githubusercontent.com/u/15113230?v=4")]
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(
|
||||
unreachable_pub,
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
rust_2018_idioms,
|
||||
clippy::string_add,
|
||||
clippy::string_add_assign,
|
||||
clippy::clone_on_ref_ptr,
|
||||
clippy::verbose_file_reads,
|
||||
clippy::unnecessary_self_imports,
|
||||
clippy::string_to_string,
|
||||
clippy::mem_forget,
|
||||
clippy::cast_lossless,
|
||||
clippy::inefficient_to_string,
|
||||
clippy::inline_always,
|
||||
clippy::linkedlist,
|
||||
clippy::macro_use_imports,
|
||||
clippy::manual_assert,
|
||||
clippy::unnecessary_join,
|
||||
clippy::wildcard_imports,
|
||||
clippy::str_to_string,
|
||||
clippy::empty_structs_with_brackets,
|
||||
clippy::zero_sized_map_values,
|
||||
clippy::manual_let_else,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::unnecessary_wraps,
|
||||
clippy::doc_markdown,
|
||||
clippy::explicit_iter_loop,
|
||||
clippy::redundant_closure_for_method_calls,
|
||||
// Rust 1.86: clippy::unnecessary_semicolon,
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#![deny(missing_docs, unsafe_code, unstable_features)]
|
||||
#[cfg(not(lettre_ignore_tls_mismatch))]
|
||||
mod compiletime_checks {
|
||||
#[cfg(all(feature = "rustls", not(feature = "aws-lc-rs"), not(feature = "ring")))]
|
||||
compile_error!(
|
||||
"feature `rustls` also requires either the `aws-lc-rs` or the `ring` feature to
|
||||
be enabled"
|
||||
);
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate rustc_serialize;
|
||||
extern crate crypto;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
extern crate email as email_format;
|
||||
extern crate bufstream;
|
||||
extern crate openssl;
|
||||
#[cfg(all(
|
||||
feature = "rustls",
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs"),
|
||||
not(feature = "webpki-roots")
|
||||
))]
|
||||
compile_error!(
|
||||
"feature `rustls` also requires either the `rustls-platform-verifier`, the `rustls-native-certs`
|
||||
or the `webpki-roots` feature to be enabled"
|
||||
);
|
||||
|
||||
#[cfg(all(feature = "native-tls", feature = "boring-tls"))]
|
||||
compile_error!("feature \"native-tls\" and feature \"boring-tls\" cannot be enabled at the same time, otherwise
|
||||
the executable will fail to link.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "native-tls",
|
||||
not(feature = "tokio1-native-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `native-tls` features, but the `tokio1-native-tls` feature hasn't been turned on.
|
||||
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(feature = "tokio1", feature = "rustls", not(feature = "tokio1-rustls")))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `rustls` features, but the `tokio1-rustls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
feature = "boring-tls",
|
||||
not(feature = "tokio1-boring-tls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `tokio1` and the `boring-tls` features, but the `tokio1-boring-tls` feature hasn't been turned on.
|
||||
If you'd like to use `boring-tls` make sure that the `rustls` feature hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(feature = "async-std1", feature = "native-tls"))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `native-tls` features, but the async-std integration doesn't support native-tls yet.
|
||||
If you'd like to work on the issue please take a look at https://github.com/lettre/lettre/issues/576.
|
||||
If you were trying to opt into `rustls` and did not activate `native-tls`, disable the default-features of lettre in `Cargo.toml` and manually add the required features.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
feature = "rustls",
|
||||
not(feature = "async-std1-rustls")
|
||||
))]
|
||||
compile_error!("Lettre is being built with the `async-std1` and the `rustls` features, but the `async-std1-rustls` feature hasn't been turned on.
|
||||
If you'd like to use `native-tls` make sure that the `rustls` hasn't been enabled by mistake.
|
||||
Make sure to apply the same to any of your crate dependencies that use the `lettre` crate.");
|
||||
}
|
||||
|
||||
pub mod address;
|
||||
#[cfg(any(feature = "smtp-transport", feature = "dkim"))]
|
||||
mod base64;
|
||||
pub mod error;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod executor;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
pub mod message;
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls_crypto;
|
||||
mod time;
|
||||
pub mod transport;
|
||||
pub mod email;
|
||||
|
||||
use std::error::Error as StdError;
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::executor::AsyncStd1Executor;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::executor::Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::executor::Tokio1Executor;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[doc(inline)]
|
||||
pub use self::transport::AsyncTransport;
|
||||
pub use crate::address::Address;
|
||||
#[cfg(feature = "builder")]
|
||||
#[doc(inline)]
|
||||
pub use crate::message::Message;
|
||||
#[cfg(all(
|
||||
feature = "file-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::AsyncFileTransport;
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(all(
|
||||
feature = "sendmail-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::AsyncSendmailTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio1", feature = "async-std1")
|
||||
))]
|
||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::SmtpTransport;
|
||||
#[doc(inline)]
|
||||
pub use crate::transport::Transport;
|
||||
use crate::{address::Envelope, error::Error};
|
||||
|
||||
pub(crate) type BoxError = Box<dyn StdError + Send + Sync>;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::{header, header::Headers, Mailbox, Mailboxes};
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From(from));
|
||||
headers.set(header::To(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_sender() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
let to = Mailboxes::new().with("amousset@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
headers.set(header::To::from(to));
|
||||
|
||||
assert_eq!(
|
||||
Envelope::try_from(&headers).unwrap(),
|
||||
Envelope::new(
|
||||
Some(Address::new("kayo2", "example.com").unwrap()),
|
||||
vec![Address::new("amousset", "example.com").unwrap()]
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn envelope_from_headers_no_to() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
let sender = Mailbox::new(None, "kayo2@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(header::From::from(from));
|
||||
headers.set(header::Sender::from(sender));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
}
|
||||
|
||||
145
src/message/attachment.rs
Normal file
145
src/message/attachment.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use crate::message::{
|
||||
header::{self, ContentType},
|
||||
IntoBody, SinglePart,
|
||||
};
|
||||
|
||||
/// `SinglePart` builder for attachments
|
||||
///
|
||||
/// Allows building attachment parts easily.
|
||||
#[derive(Clone)]
|
||||
pub struct Attachment {
|
||||
disposition: Disposition,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Disposition {
|
||||
/// File name
|
||||
Attached(String),
|
||||
/// Content id
|
||||
Inline(String),
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
/// Create a new attachment
|
||||
///
|
||||
/// This attachment will be displayed as a normal attachment,
|
||||
/// with the chosen `filename` appearing as the file name.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let filename = String::from("invoice.pdf");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("invoice.pdf")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("application/pdf").unwrap();
|
||||
/// let attachment = Attachment::new(filename).body(filebody, content_type);
|
||||
///
|
||||
/// // The document `attachment` will show up as a normal attachment.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(filename: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Attached(filename),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new inline attachment
|
||||
///
|
||||
/// This attachment should be displayed inline into the message
|
||||
/// body:
|
||||
///
|
||||
/// ```html
|
||||
/// <img src="cid:123">
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::error::Error;
|
||||
/// use std::fs;
|
||||
///
|
||||
/// use lettre::message::{header::ContentType, Attachment};
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let content_id = String::from("123");
|
||||
/// # if false {
|
||||
/// let filebody = fs::read("image.jpg")?;
|
||||
/// # }
|
||||
/// # let filebody = fs::read("docs/lettre.png")?;
|
||||
/// let content_type = ContentType::parse("image/jpeg").unwrap();
|
||||
/// let attachment = Attachment::new_inline(content_id).body(filebody, content_type);
|
||||
///
|
||||
/// // The image `attachment` will display inline into the email.
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new_inline(content_id: String) -> Self {
|
||||
Attachment {
|
||||
disposition: Disposition::Inline(content_id),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the attachment into a [`SinglePart`] which can then be used to build the rest of the email
|
||||
///
|
||||
/// Look at the [Complex MIME body example](crate::message#complex-mime-body)
|
||||
/// to see how [`SinglePart`] can be put into the email.
|
||||
pub fn body<T: IntoBody>(self, content: T, content_type: ContentType) -> SinglePart {
|
||||
let mut builder = SinglePart::builder();
|
||||
builder = match self.disposition {
|
||||
Disposition::Attached(filename) => {
|
||||
builder.header(header::ContentDisposition::attachment(&filename))
|
||||
}
|
||||
Disposition::Inline(content_id) => builder
|
||||
.header(header::ContentId::from(format!("<{content_id}>")))
|
||||
.header(header::ContentDisposition::inline()),
|
||||
};
|
||||
builder = builder.header(content_type);
|
||||
builder.body(content)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::message::header::ContentType;
|
||||
|
||||
#[test]
|
||||
fn attachment() {
|
||||
let part = super::Attachment::new(String::from("test.txt")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-Disposition: attachment; filename=\"test.txt\"\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachment_inline() {
|
||||
let part = super::Attachment::new_inline(String::from("id")).body(
|
||||
String::from("Hello world!"),
|
||||
ContentType::parse("text/plain").unwrap(),
|
||||
);
|
||||
assert_eq!(
|
||||
&String::from_utf8_lossy(&part.formatted()),
|
||||
concat!(
|
||||
"Content-ID: <id>\r\n",
|
||||
"Content-Disposition: inline\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n\r\n",
|
||||
"Hello world!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
575
src/message/body.rs
Normal file
575
src/message/body.rs
Normal file
@@ -0,0 +1,575 @@
|
||||
use std::{mem, ops::Deref};
|
||||
|
||||
use crate::message::header::ContentTransferEncoding;
|
||||
|
||||
/// A [`Message`][super::Message] or [`SinglePart`][super::SinglePart] body that has already been encoded.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Body {
|
||||
buf: Vec<u8>,
|
||||
encoding: ContentTransferEncoding,
|
||||
}
|
||||
|
||||
/// Either a `Vec<u8>` or a `String`.
|
||||
///
|
||||
/// If the content is valid utf-8 a `String` should be passed, as it
|
||||
/// makes for a more efficient `Content-Transfer-Encoding` to be chosen.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MaybeString {
|
||||
/// Binary data
|
||||
Binary(Vec<u8>),
|
||||
/// UTF-8 string
|
||||
String(String),
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// Encode the supplied `buf`, making it ready to be sent as a body.
|
||||
///
|
||||
/// Takes a `Vec<u8>` or a `String`.
|
||||
///
|
||||
/// Automatically chooses the most efficient encoding between
|
||||
/// `7bit`, `quoted-printable` and `base64`.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// If `buf` is valid utf-8 a `String` should be supplied, as `String`s
|
||||
/// can be encoded as `7bit` or `quoted-printable`, while `Vec<u8>` always
|
||||
/// get encoded as `base64`.
|
||||
pub fn new<B: Into<MaybeString>>(buf: B) -> Self {
|
||||
let mut buf: MaybeString = buf.into();
|
||||
|
||||
let encoding = buf.encoding(false);
|
||||
buf.encode_crlf();
|
||||
Self::new_impl(buf.into(), encoding)
|
||||
}
|
||||
|
||||
/// Encode the supplied `buf`, using the provided `encoding`.
|
||||
///
|
||||
/// [`Body::new`] is generally the better option.
|
||||
///
|
||||
/// If `String` is passed, line endings are converted to `CRLF`.
|
||||
///
|
||||
/// Returns an [`Err`] giving back the supplied `buf`, in case the chosen
|
||||
/// encoding would have resulted into `buf` being encoded
|
||||
/// into an invalid body.
|
||||
pub fn new_with_encoding<B: Into<MaybeString>>(
|
||||
buf: B,
|
||||
encoding: ContentTransferEncoding,
|
||||
) -> Result<Self, Vec<u8>> {
|
||||
let mut buf: MaybeString = buf.into();
|
||||
|
||||
let best_encoding = buf.encoding(true);
|
||||
let ok = match (encoding, best_encoding) {
|
||||
(ContentTransferEncoding::SevenBit, ContentTransferEncoding::SevenBit) => true,
|
||||
(
|
||||
ContentTransferEncoding::EightBit,
|
||||
ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit,
|
||||
) => true,
|
||||
(ContentTransferEncoding::SevenBit | ContentTransferEncoding::EightBit, _) => false,
|
||||
(
|
||||
ContentTransferEncoding::QuotedPrintable
|
||||
| ContentTransferEncoding::Base64
|
||||
| ContentTransferEncoding::Binary,
|
||||
_,
|
||||
) => true,
|
||||
};
|
||||
if !ok {
|
||||
return Err(buf.into());
|
||||
}
|
||||
|
||||
buf.encode_crlf();
|
||||
Ok(Self::new_impl(buf.into(), encoding))
|
||||
}
|
||||
|
||||
/// Builds a new `Body` using a pre-encoded buffer.
|
||||
///
|
||||
/// **Generally not you want.**
|
||||
///
|
||||
/// `buf` shouldn't contain non-ascii characters, lines longer than 1000 characters or nul bytes.
|
||||
#[inline]
|
||||
pub fn dangerous_pre_encoded(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
|
||||
Self { buf, encoding }
|
||||
}
|
||||
|
||||
/// Encodes the supplied `buf` using the provided `encoding`
|
||||
fn new_impl(buf: Vec<u8>, encoding: ContentTransferEncoding) -> Self {
|
||||
match encoding {
|
||||
ContentTransferEncoding::SevenBit
|
||||
| ContentTransferEncoding::EightBit
|
||||
| ContentTransferEncoding::Binary => Self { buf, encoding },
|
||||
ContentTransferEncoding::QuotedPrintable => {
|
||||
let encoded = quoted_printable::encode(buf);
|
||||
|
||||
Self::dangerous_pre_encoded(encoded, ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
ContentTransferEncoding::Base64 => {
|
||||
let len = email_encoding::body::base64::encoded_len(buf.len());
|
||||
|
||||
let mut out = String::with_capacity(len);
|
||||
email_encoding::body::base64::encode(&buf, &mut out)
|
||||
.expect("encode body as base64");
|
||||
|
||||
Self::dangerous_pre_encoded(out.into_bytes(), ContentTransferEncoding::Base64)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the length of this `Body` in bytes.
|
||||
#[inline]
|
||||
pub fn len(&self) -> usize {
|
||||
self.buf.len()
|
||||
}
|
||||
|
||||
/// Returns `true` if this `Body` has a length of zero, `false` otherwise.
|
||||
#[inline]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.buf.is_empty()
|
||||
}
|
||||
|
||||
/// Returns the `Content-Transfer-Encoding` of this `Body`.
|
||||
#[inline]
|
||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// Consumes `Body` and returns the inner `Vec<u8>`
|
||||
#[inline]
|
||||
pub fn into_vec(self) -> Vec<u8> {
|
||||
self.buf
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeString {
|
||||
/// Suggests the best `Content-Transfer-Encoding` to be used for this `MaybeString`
|
||||
///
|
||||
/// The `binary` encoding is never returned
|
||||
fn encoding(&self, supports_utf8: bool) -> ContentTransferEncoding {
|
||||
use email_encoding::body::Encoding;
|
||||
|
||||
let output = match self {
|
||||
Self::String(s) => Encoding::choose(s.as_str(), supports_utf8),
|
||||
Self::Binary(b) => Encoding::choose(b.as_slice(), supports_utf8),
|
||||
};
|
||||
|
||||
match output {
|
||||
Encoding::SevenBit => ContentTransferEncoding::SevenBit,
|
||||
Encoding::EightBit => ContentTransferEncoding::EightBit,
|
||||
Encoding::QuotedPrintable => ContentTransferEncoding::QuotedPrintable,
|
||||
Encoding::Base64 => ContentTransferEncoding::Base64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Encode line endings to CRLF if the variant is `String`
|
||||
fn encode_crlf(&mut self) {
|
||||
match self {
|
||||
Self::String(string) => in_place_crlf_line_endings(string),
|
||||
Self::Binary(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for something that takes an encoded [`Body`].
|
||||
///
|
||||
/// Used by [`MessageBuilder::body`][super::MessageBuilder::body] and
|
||||
/// [`SinglePartBuilder::body`][super::SinglePartBuilder::body],
|
||||
/// which can either take something that can be encoded into [`Body`]
|
||||
/// or a pre-encoded [`Body`].
|
||||
///
|
||||
/// If `encoding` is `None` the best encoding between `7bit`, `quoted-printable`
|
||||
/// and `base64` is chosen based on the input body. **Best option.**
|
||||
///
|
||||
/// If `encoding` is `Some` the supplied encoding is used.
|
||||
/// **NOTE:** if using the specified `encoding` would result into a malformed
|
||||
/// body, this will panic!
|
||||
pub trait IntoBody {
|
||||
/// Encode as valid body
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body;
|
||||
}
|
||||
|
||||
impl<T> IntoBody for T
|
||||
where
|
||||
T: Into<MaybeString>,
|
||||
{
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
|
||||
match encoding {
|
||||
Some(encoding) => Body::new_with_encoding(self, encoding).expect("invalid encoding"),
|
||||
None => Body::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoBody for Body {
|
||||
fn into_body(self, encoding: Option<ContentTransferEncoding>) -> Body {
|
||||
let _ = encoding;
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Body {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.buf.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for MaybeString {
|
||||
#[inline]
|
||||
fn from(b: Vec<u8>) -> Self {
|
||||
Self::Binary(b)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for MaybeString {
|
||||
#[inline]
|
||||
fn from(s: String) -> Self {
|
||||
Self::String(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MaybeString> for Vec<u8> {
|
||||
#[inline]
|
||||
fn from(s: MaybeString) -> Self {
|
||||
match s {
|
||||
MaybeString::Binary(b) => b,
|
||||
MaybeString::String(s) => s.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MaybeString {
|
||||
type Target = [u8];
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
match self {
|
||||
Self::Binary(b) => b.as_ref(),
|
||||
Self::String(s) => s.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// In place conversion to CRLF line endings
|
||||
fn in_place_crlf_line_endings(string: &mut String) {
|
||||
let indices = find_all_lf_char_indices(string);
|
||||
|
||||
for i in indices {
|
||||
// this relies on `indices` being in reverse order
|
||||
string.insert(i, '\r');
|
||||
}
|
||||
}
|
||||
|
||||
/// Find indices to all places where `\r` should be inserted
|
||||
/// in order to make `s` have CRLF line endings
|
||||
///
|
||||
/// The list is reversed, which is more efficient.
|
||||
fn find_all_lf_char_indices(s: &str) -> Vec<usize> {
|
||||
let mut indices = Vec::new();
|
||||
|
||||
let mut found_lf = false;
|
||||
for (i, c) in s.char_indices().rev() {
|
||||
if mem::take(&mut found_lf) && c != '\r' {
|
||||
// the previous character was `\n`, but this isn't a `\r`
|
||||
indices.push(i + c.len_utf8());
|
||||
}
|
||||
|
||||
found_lf = c == '\n';
|
||||
}
|
||||
|
||||
if found_lf {
|
||||
// the first character is `\n`
|
||||
indices.push(0);
|
||||
}
|
||||
|
||||
indices
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{in_place_crlf_line_endings, Body, ContentTransferEncoding};
|
||||
|
||||
#[test]
|
||||
fn seven_bit_detect() {
|
||||
let encoded = Body::new(String::from("Hello, world!"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_encode() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello, world!"),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::SevenBit);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_detect() {
|
||||
let encoded = Body::new("Hello, world!".repeat(100));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
|
||||
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
|
||||
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
|
||||
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
|
||||
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
|
||||
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
|
||||
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
|
||||
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
|
||||
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_fail() {
|
||||
let result = Body::new_with_encoding(
|
||||
"Hello, world!".repeat(100),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_too_long_encode_quotedprintable() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
"Hello, world!".repeat(100),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, worl=\r\n",
|
||||
"d!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, w=\r\n",
|
||||
"orld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello=\r\n",
|
||||
", world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!He=\r\n",
|
||||
"llo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world=\r\n",
|
||||
"!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wo=\r\n",
|
||||
"rld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello,=\r\n",
|
||||
" world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hel=\r\n",
|
||||
"lo, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!=\r\n",
|
||||
"Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, wor=\r\n",
|
||||
"ld!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, =\r\n",
|
||||
"world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!Hell=\r\n",
|
||||
"o, world!Hello, world!Hello, world!Hello, world!Hello, world!Hello, world!H=\r\n",
|
||||
"ello, world!Hello, world!"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seven_bit_invalid() {
|
||||
let result = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::SevenBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_encode() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::EightBit,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::EightBit);
|
||||
assert_eq!(encoded.as_ref(), "Привет, мир!".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eight_bit_too_long_fail() {
|
||||
let result = Body::new_with_encoding(
|
||||
"Привет, мир!".repeat(200),
|
||||
ContentTransferEncoding::EightBit,
|
||||
);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_detect() {
|
||||
let encoded = Body::new(String::from("Questo messaggio è corto"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(encoded.as_ref(), b"Questo messaggio =C3=A8 corto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_ascii() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello, world!"),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(encoded.as_ref(), b"Hello, world!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_utf8() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Привет, мир!"),
|
||||
ContentTransferEncoding::QuotedPrintable,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
b"=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D1=82, =D0=BC=D0=B8=D1=80!".as_ref()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoted_printable_encode_line_wrap() {
|
||||
let encoded = Body::new(String::from(
|
||||
"Se lo standard 📬 fosse stato più semplice avremmo finito molto prima.",
|
||||
));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
println!("{}", std::str::from_utf8(encoded.as_ref()).unwrap());
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"Se lo standard =F0=9F=93=AC fosse stato pi=C3=B9 semplice avremmo finito mo=\r\n",
|
||||
"lto prima."
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_detect() {
|
||||
let input = Body::new(vec![0; 80]);
|
||||
let encoding = input.encoding();
|
||||
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes() {
|
||||
let encoded =
|
||||
Body::new_with_encoding(vec![0; 80], ContentTransferEncoding::Base64).unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n",
|
||||
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes_wrapping() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].repeat(20),
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"AAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUG\r\n",
|
||||
"BwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQID\r\n",
|
||||
"BAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkAAQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAkA\r\n",
|
||||
"AQIDBAUGBwgJAAECAwQFBgcICQABAgMEBQYHCAk="
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_ascii() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
String::from("Hello World!"),
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(encoded.as_ref(), b"SGVsbG8gV29ybGQh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_ascii_wrapping() {
|
||||
let encoded =
|
||||
Body::new_with_encoding("Hello World!".repeat(20), ContentTransferEncoding::Base64)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"SGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29y\r\n",
|
||||
"bGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8g\r\n",
|
||||
"V29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVs\r\n",
|
||||
"bG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQhSGVsbG8gV29ybGQh\r\n",
|
||||
"SGVsbG8gV29ybGQh"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf() {
|
||||
let mut string = String::from("Send me a ✉️\nwith\nlettre!\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "Send me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harsh_crlf() {
|
||||
let mut string = String::from("\n\nSend me a ✉️\r\n\nwith\n\nlettre!\n\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(
|
||||
string,
|
||||
"\r\n\r\nSend me a ✉️\r\n\r\nwith\r\n\r\nlettre!\r\n\r\n😀"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_noop() {
|
||||
let mut string = String::from("\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
|
||||
in_place_crlf_line_endings(&mut string);
|
||||
assert_eq!(string, "\r\nSend me a ✉️\r\nwith\r\nlettre!\r\n😀");
|
||||
}
|
||||
}
|
||||
615
src/message/dkim.rs
Normal file
615
src/message/dkim.rs
Normal file
@@ -0,0 +1,615 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use ed25519_dalek::Signer;
|
||||
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs1v15::Pkcs1v15Sign, RsaPrivateKey};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::message::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Headers, Message,
|
||||
};
|
||||
|
||||
/// Describe Dkim Canonicalization to apply to either body or headers
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum DkimCanonicalizationType {
|
||||
Simple,
|
||||
Relaxed,
|
||||
}
|
||||
|
||||
impl Display for DkimCanonicalizationType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
DkimCanonicalizationType::Simple => "simple",
|
||||
DkimCanonicalizationType::Relaxed => "relaxed",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe Canonicalization to be applied before signing
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DkimCanonicalization {
|
||||
pub header: DkimCanonicalizationType,
|
||||
pub body: DkimCanonicalizationType,
|
||||
}
|
||||
|
||||
impl Default for DkimCanonicalization {
|
||||
fn default() -> Self {
|
||||
DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Simple,
|
||||
body: DkimCanonicalizationType::Relaxed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Format canonicalization to be shown in Dkim header
|
||||
impl Display for DkimCanonicalization {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}/{}", self.header, self.body)
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe the algorithm used for signing the message
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum DkimSigningAlgorithm {
|
||||
Rsa,
|
||||
Ed25519,
|
||||
}
|
||||
|
||||
impl Display for DkimSigningAlgorithm {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self {
|
||||
DkimSigningAlgorithm::Rsa => "rsa",
|
||||
DkimSigningAlgorithm::Ed25519 => "ed25519",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe [`DkimSigningKey`] key error
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKeyError(InnerDkimSigningKeyError);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerDkimSigningKeyError {
|
||||
Base64(base64::DecodeError),
|
||||
Rsa(rsa::pkcs1::Error),
|
||||
Ed25519(ed25519_dalek::ed25519::Error),
|
||||
}
|
||||
|
||||
impl Display for DkimSigningKeyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match &self.0 {
|
||||
InnerDkimSigningKeyError::Base64(_err) => "base64 decode error",
|
||||
InnerDkimSigningKeyError::Rsa(_err) => "rsa decode error",
|
||||
InnerDkimSigningKeyError::Ed25519(_err) => "ed25519 decode error",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for DkimSigningKeyError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(match &self.0 {
|
||||
InnerDkimSigningKeyError::Base64(err) => err,
|
||||
InnerDkimSigningKeyError::Rsa(err) => err,
|
||||
InnerDkimSigningKeyError::Ed25519(err) => err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Describe a signing key to be carried by [`DkimConfig`] struct
|
||||
#[derive(Debug)]
|
||||
pub struct DkimSigningKey(InnerDkimSigningKey);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum InnerDkimSigningKey {
|
||||
Rsa(RsaPrivateKey),
|
||||
Ed25519(ed25519_dalek::SigningKey),
|
||||
}
|
||||
|
||||
impl DkimSigningKey {
|
||||
pub fn new(
|
||||
private_key: &str,
|
||||
algorithm: DkimSigningAlgorithm,
|
||||
) -> Result<DkimSigningKey, DkimSigningKeyError> {
|
||||
Ok(Self(match algorithm {
|
||||
DkimSigningAlgorithm::Rsa => InnerDkimSigningKey::Rsa(
|
||||
RsaPrivateKey::from_pkcs1_pem(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Rsa(err)))?,
|
||||
),
|
||||
DkimSigningAlgorithm::Ed25519 => {
|
||||
InnerDkimSigningKey::Ed25519(ed25519_dalek::SigningKey::from_bytes(
|
||||
&crate::base64::decode(private_key)
|
||||
.map_err(|err| DkimSigningKeyError(InnerDkimSigningKeyError::Base64(err)))?
|
||||
.try_into()
|
||||
.map_err(|_| {
|
||||
DkimSigningKeyError(InnerDkimSigningKeyError::Ed25519(
|
||||
ed25519_dalek::ed25519::Error::new(),
|
||||
))
|
||||
})?,
|
||||
))
|
||||
}
|
||||
}))
|
||||
}
|
||||
fn get_signing_algorithm(&self) -> DkimSigningAlgorithm {
|
||||
match self.0 {
|
||||
InnerDkimSigningKey::Rsa(_) => DkimSigningAlgorithm::Rsa,
|
||||
InnerDkimSigningKey::Ed25519(_) => DkimSigningAlgorithm::Ed25519,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct to describe Dkim configuration applied when signing a message
|
||||
#[derive(Debug)]
|
||||
pub struct DkimConfig {
|
||||
/// The name of the key published in DNS
|
||||
selector: String,
|
||||
/// The domain for which we sign the message
|
||||
domain: String,
|
||||
/// The private key in PKCS1 string format
|
||||
private_key: DkimSigningKey,
|
||||
/// A list of header names to be included in the signature. Signing of more than one
|
||||
/// header with the same name is not supported
|
||||
headers: Vec<HeaderName>,
|
||||
/// The signing algorithm to be used when signing
|
||||
canonicalization: DkimCanonicalization,
|
||||
}
|
||||
|
||||
impl DkimConfig {
|
||||
/// Create a default signature configuration with a set of headers and "simple/relaxed"
|
||||
/// canonicalization
|
||||
pub fn default_config(
|
||||
selector: String,
|
||||
domain: String,
|
||||
private_key: DkimSigningKey,
|
||||
) -> DkimConfig {
|
||||
DkimConfig {
|
||||
selector,
|
||||
domain,
|
||||
private_key,
|
||||
headers: vec![
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
],
|
||||
canonicalization: DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Simple,
|
||||
body: DkimCanonicalizationType::Relaxed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`DkimConfig`]
|
||||
pub fn new(
|
||||
selector: String,
|
||||
domain: String,
|
||||
private_key: DkimSigningKey,
|
||||
headers: Vec<HeaderName>,
|
||||
canonicalization: DkimCanonicalization,
|
||||
) -> DkimConfig {
|
||||
DkimConfig {
|
||||
selector,
|
||||
domain,
|
||||
private_key,
|
||||
headers,
|
||||
canonicalization,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a Headers struct with a Dkim-Signature Header created from given parameters
|
||||
fn dkim_header_format(
|
||||
config: &DkimConfig,
|
||||
timestamp: u64,
|
||||
headers_list: &str,
|
||||
body_hash: &str,
|
||||
signature: &str,
|
||||
) -> Headers {
|
||||
let mut headers = Headers::new();
|
||||
let header_name =
|
||||
dkim_canonicalize_header_tag("DKIM-Signature", config.canonicalization.header);
|
||||
let header_name = HeaderName::new_from_ascii(header_name.into()).unwrap();
|
||||
headers.insert_raw(HeaderValue::new(header_name, format!("v=1; a={signing_algorithm}-sha256; d={domain}; s={selector}; c={canon}; q=dns/txt; t={timestamp}; h={headers_list}; bh={body_hash}; b={signature}",domain=config.domain, selector=config.selector,canon=config.canonicalization,timestamp=timestamp,headers_list=headers_list,body_hash=body_hash,signature=signature,signing_algorithm=config.private_key.get_signing_algorithm())));
|
||||
headers
|
||||
}
|
||||
|
||||
/// Canonicalize the body of an email
|
||||
fn dkim_canonicalize_body(
|
||||
mut body: &[u8],
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> Cow<'_, [u8]> {
|
||||
match canonicalization {
|
||||
DkimCanonicalizationType::Simple => {
|
||||
// Remove empty lines at end
|
||||
while body.ends_with(b"\r\n\r\n") {
|
||||
body = &body[..body.len() - 2];
|
||||
}
|
||||
Cow::Borrowed(body)
|
||||
}
|
||||
DkimCanonicalizationType::Relaxed => {
|
||||
let mut out = Vec::with_capacity(body.len());
|
||||
loop {
|
||||
match body {
|
||||
[b' ' | b'\t', b'\r', b'\n', ..] => {}
|
||||
[b' ' | b'\t', b' ' | b'\t', ..] => {}
|
||||
[b' ' | b'\t', ..] => out.push(b' '),
|
||||
[c, ..] => out.push(*c),
|
||||
[] => break,
|
||||
}
|
||||
body = &body[1..];
|
||||
}
|
||||
// Remove empty lines at end
|
||||
while out.ends_with(b"\r\n\r\n") {
|
||||
out.truncate(out.len() - 2);
|
||||
}
|
||||
Cow::Owned(out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dkim_canonicalize_headers_relaxed(headers: &str) -> String {
|
||||
let mut r = String::with_capacity(headers.len());
|
||||
|
||||
fn skip_whitespace(h: &str) -> &str {
|
||||
match h.as_bytes().first() {
|
||||
Some(b' ' | b'\t') => skip_whitespace(&h[1..]),
|
||||
_ => h,
|
||||
}
|
||||
}
|
||||
|
||||
fn name(h: &str, out: &mut String) {
|
||||
if let Some(name_end) = h.bytes().position(|c| c == b':') {
|
||||
let (name, rest) = h.split_at(name_end + 1);
|
||||
*out += name;
|
||||
// Space after header colon is stripped.
|
||||
value(skip_whitespace(rest), out);
|
||||
} else {
|
||||
// This should never happen.
|
||||
*out += h;
|
||||
}
|
||||
}
|
||||
|
||||
fn value(h: &str, out: &mut String) {
|
||||
match h.as_bytes() {
|
||||
// Continuation lines.
|
||||
[b'\r', b'\n', b' ' | b'\t', ..] => {
|
||||
out.push(' ');
|
||||
value(skip_whitespace(&h[2..]), out);
|
||||
}
|
||||
// End of header.
|
||||
[b'\r', b'\n', ..] => {
|
||||
*out += "\r\n";
|
||||
name(&h[2..], out);
|
||||
}
|
||||
// Sequential whitespace.
|
||||
[b' ' | b'\t', b' ' | b'\t' | b'\r', ..] => value(&h[1..], out),
|
||||
// All whitespace becomes spaces.
|
||||
[b'\t', ..] => {
|
||||
out.push(' ');
|
||||
value(&h[1..], out);
|
||||
}
|
||||
[_, ..] => {
|
||||
let mut chars = h.chars();
|
||||
out.push(chars.next().unwrap());
|
||||
value(chars.as_str(), out);
|
||||
}
|
||||
[] => {}
|
||||
}
|
||||
}
|
||||
|
||||
name(headers, &mut r);
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
/// Canonicalize header tag
|
||||
fn dkim_canonicalize_header_tag(
|
||||
name: &str,
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> Cow<'_, str> {
|
||||
match canonicalization {
|
||||
DkimCanonicalizationType::Simple => Cow::Borrowed(name),
|
||||
DkimCanonicalizationType::Relaxed => Cow::Owned(name.to_lowercase()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Canonicalize signed headers passed as `headers_list` among `mail_headers` using canonicalization
|
||||
fn dkim_canonicalize_headers<'a>(
|
||||
headers_list: impl IntoIterator<Item = &'a str>,
|
||||
mail_headers: &Headers,
|
||||
canonicalization: DkimCanonicalizationType,
|
||||
) -> String {
|
||||
let mut covered_headers = Headers::new();
|
||||
for name in headers_list {
|
||||
if let Some(h) = mail_headers.find_header(name) {
|
||||
let name = dkim_canonicalize_header_tag(name, canonicalization);
|
||||
covered_headers.insert_raw(HeaderValue::dangerous_new_pre_encoded(
|
||||
HeaderName::new_from_ascii(name.into()).unwrap(),
|
||||
h.get_raw().into(),
|
||||
h.get_encoded().into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let serialized = covered_headers.to_string();
|
||||
|
||||
match canonicalization {
|
||||
DkimCanonicalizationType::Simple => serialized,
|
||||
DkimCanonicalizationType::Relaxed => dkim_canonicalize_headers_relaxed(&serialized),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign with Dkim a message by adding Dkim-Signature header created with configuration expressed by
|
||||
/// `dkim_config`
|
||||
pub fn dkim_sign(message: &mut Message, dkim_config: &DkimConfig) {
|
||||
dkim_sign_fixed_time(message, dkim_config, crate::time::now());
|
||||
}
|
||||
|
||||
fn dkim_sign_fixed_time(message: &mut Message, dkim_config: &DkimConfig, timestamp: SystemTime) {
|
||||
let timestamp = timestamp
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
let headers = message.headers();
|
||||
let body_hash = Sha256::digest(dkim_canonicalize_body(
|
||||
&message.body_raw(),
|
||||
dkim_config.canonicalization.body,
|
||||
));
|
||||
let bh = crate::base64::encode(body_hash);
|
||||
let mut signed_headers_list =
|
||||
dkim_config
|
||||
.headers
|
||||
.iter()
|
||||
.fold(String::new(), |mut list, header| {
|
||||
if !list.is_empty() {
|
||||
list.push(':');
|
||||
}
|
||||
|
||||
list.push_str(header);
|
||||
list
|
||||
});
|
||||
if let DkimCanonicalizationType::Relaxed = dkim_config.canonicalization.header {
|
||||
signed_headers_list.make_ascii_lowercase();
|
||||
}
|
||||
let dkim_header = dkim_header_format(dkim_config, timestamp, &signed_headers_list, &bh, "");
|
||||
let signed_headers = dkim_canonicalize_headers(
|
||||
dkim_config.headers.iter().map(AsRef::as_ref),
|
||||
headers,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
let canonicalized_dkim_header = dkim_canonicalize_headers(
|
||||
["DKIM-Signature"],
|
||||
&dkim_header,
|
||||
dkim_config.canonicalization.header,
|
||||
);
|
||||
let mut hashed_headers = Sha256::new();
|
||||
hashed_headers.update(signed_headers.as_bytes());
|
||||
hashed_headers.update(canonicalized_dkim_header.trim_end().as_bytes());
|
||||
let hashed_headers = hashed_headers.finalize();
|
||||
let signature = match &dkim_config.private_key.0 {
|
||||
InnerDkimSigningKey::Rsa(private_key) => crate::base64::encode(
|
||||
private_key
|
||||
.sign(Pkcs1v15Sign::new::<Sha256>(), &hashed_headers)
|
||||
.unwrap(),
|
||||
),
|
||||
InnerDkimSigningKey::Ed25519(private_key) => {
|
||||
crate::base64::encode(private_key.sign(&hashed_headers).to_bytes())
|
||||
}
|
||||
};
|
||||
let dkim_header = dkim_header_format(
|
||||
dkim_config,
|
||||
timestamp,
|
||||
&signed_headers_list,
|
||||
&bh,
|
||||
&signature,
|
||||
);
|
||||
message.headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("DKIM-Signature"),
|
||||
dkim_header.get_raw("DKIM-Signature").unwrap().to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{
|
||||
super::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Header, Message,
|
||||
},
|
||||
dkim_canonicalize_body, dkim_canonicalize_headers, dkim_sign_fixed_time,
|
||||
DkimCanonicalization, DkimCanonicalizationType, DkimConfig, DkimSigningAlgorithm,
|
||||
DkimSigningKey,
|
||||
};
|
||||
use crate::StdError;
|
||||
|
||||
const KEY_RSA: &str = "-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEAwOsW7UFcWn1ch3UM8Mll5qZH5hVHKJQ8Z0tUlebUECq0vjw6
|
||||
VcsIucZ/B70VpCN63whyi7oApdCIS1o0zad7f0UaW/BfxXADqdcFL36uMaG0RHer
|
||||
uSASjQGnsl9Kozt/dXiDZX5ngjr/arLJhNZSNR4/9VSwqbE2OPXaSaQ9BsqneD0P
|
||||
8dCVSfkkDZCcfC2864z7hvC01lFzWQKF36ZAoGBERHScHtFMAzUOgGuqqPiP5khw
|
||||
DQB3Ffccf+BsWLU2OOteshUwTGjpoangbPCYj6kckwNm440lQwuqTinpC92yyIE5
|
||||
Ol8psNMW49DLowAeZb6JrjLhD+wY9bghTaOkcwIDAQABAoIBAHTZ8LkkrdvhsvoZ
|
||||
XA088AwVC9fBa6iYoT2v0zw45JomQ/Q2Zt8wa8ibAradQU56byJI65jWwS2ucd+y
|
||||
c+ldWOBt6tllb50XjCCDrRBnmvtVBuux0MIBOztNlVXlgj/8+ecdZ/lB51Bqi+sF
|
||||
ACsF5iVmfTcMZTVjsYQu5llUseI6Lwgqpx6ktaXD2PVsVo9Gf01ssZ4GCy69wB/3
|
||||
20CsOz4LEpSYkq1oE98lMMGCfD7py3L9kWHYNNisam78GM+1ynRxRGwEDUbz6pxs
|
||||
fGPIAwHLaZsOmibPkBB0PJTW742w86qQ8KAqC6ZbRYOF19rSMj3oTfRnPMHn9Uu5
|
||||
N8eQcoECgYEA97SMUrz2hqII5i8igKylO9kV8pjcIWKI0rdt8MKj4FXTNYjjO9I+
|
||||
41ONOjhUOpFci/G3YRKi8UiwbKxIRTvIxNMh2xj6Ws3iO9gQHK1j8xTWxJdjEBEz
|
||||
EuZI59Mi5H7fxSL1W+n8nS8JVsaH93rvQErngqTUAsihAzjxHWdFwm0CgYEAx2Dh
|
||||
claESJP2cOKgYp+SUNwc26qMaqnl1f37Yn+AflrQOfgQqJe5TRbicEC+nFlm6XUt
|
||||
3st1Nj29H0uOMmMZDmDCO+cOs5Qv5A9pG6jSC6wM+2KNHQDtrxlakBFygePEPVVy
|
||||
GXaY9DRa9Q4/4ataxDR2/VvIAWfEEtMTJIBDtl8CgYAIXEuwLziS6r0qJ8UeWrVp
|
||||
A7a97XLgnZbIpfBMBAXL+JmcYPZqenos6hEGOgh9wZJCFvJ9kEd3pWBvCpGV5KKu
|
||||
IgIuhvVMQ06zfmNs1F1fQwDMud9aF3qF1Mf5KyMuWynqWXe2lns0QvYpu6GzNK8G
|
||||
mICf5DhTr7nfhfh9aZLtMQKBgCxKsmqzG5n//MxhHB4sstVxwJtwDNeZPKzISnM8
|
||||
PfBT/lQSbqj1Y73japRjXbTgC4Ore3A2JKjTGFN+dm1tJGDUT/H8x4BPWEBCyCfT
|
||||
3i2noA6sewrJbQPsDvlYVubSEYNKmxlbBmmhw98StlBMv9I8kX6BSDI/uggwid0e
|
||||
/WvjAoGBAKpZ0UOKQyrl9reBiUfrpRCvIMakBMd79kNiH+5y0Soq/wCAnAuABayj
|
||||
XEIBhFv+HxeLEnT7YV+Zzqp5L9kKw/EU4ik3JX/XsEihdSxEuGX00ZYOw05FEfpW
|
||||
cJ5Ku0OTwRtSMaseRPX+T4EfG1Caa/eunPPN4rh+CSup2BVVarOT
|
||||
-----END RSA PRIVATE KEY-----";
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestHeader(String);
|
||||
|
||||
impl Header for TestHeader {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Test")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, Box<dyn StdError + Send + Sync>> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
fn test_message() -> Message {
|
||||
Message::builder()
|
||||
.from("Test O'Leary <test+ezrz@example.net>".parse().unwrap())
|
||||
.to("Test2 <test2@example.org>".parse().unwrap())
|
||||
.date(std::time::UNIX_EPOCH)
|
||||
.header(TestHeader("test test very very long with spaces and extra spaces \twill be folded to several lines ".to_owned()))
|
||||
.subject("Test with utf-8 ë")
|
||||
.body("test\r\n\r\ntest \ttest\r\n\r\n\r\n".to_owned()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_simple_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Simple), "From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\nTest: test test very very long with spaces and extra spaces \twill be\r\n folded to several lines \r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_headers_relaxed_canonicalize() {
|
||||
let message = test_message();
|
||||
dbg!(message.headers.to_string());
|
||||
assert_eq!(dkim_canonicalize_headers(["From", "Test"], &message.headers, DkimCanonicalizationType::Relaxed),"from:=?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\ntest:test test very very long with spaces and extra spaces will be folded to several lines\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_body_simple_canonicalize() {
|
||||
let body = b" C \r\nD \t E\r\n\r\n\r\n";
|
||||
assert_eq!(
|
||||
dkim_canonicalize_body(body, DkimCanonicalizationType::Simple).into_owned(),
|
||||
b" C \r\nD \t E\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_body_relaxed_canonicalize() {
|
||||
let body = b" C \r\nD \t E\r\n\tF\r\n\t\r\n\r\n\r\n";
|
||||
assert_eq!(
|
||||
dkim_canonicalize_body(body, DkimCanonicalizationType::Relaxed).into_owned(),
|
||||
b" C\r\nD E\r\n F\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_rsa_simple() {
|
||||
let mut message = test_message();
|
||||
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
|
||||
dkim_sign_fixed_time(
|
||||
&mut message,
|
||||
&DkimConfig::new(
|
||||
"dkimtest".to_owned(),
|
||||
"example.org".to_owned(),
|
||||
signing_key,
|
||||
vec![
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
],
|
||||
DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Simple,
|
||||
body: DkimCanonicalizationType::Simple,
|
||||
},
|
||||
),
|
||||
std::time::UNIX_EPOCH,
|
||||
);
|
||||
let signed = message.formatted();
|
||||
let signed = std::str::from_utf8(&signed).unwrap();
|
||||
assert_eq!(
|
||||
signed,
|
||||
std::concat!(
|
||||
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
||||
"To: Test2 <test2@example.org>\r\n",
|
||||
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
|
||||
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
|
||||
" folded to several lines \r\n",
|
||||
"Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
|
||||
" c=simple/simple; q=dns/txt; t=0; h=Date:From:Subject:To;\r\n",
|
||||
" bh=f3Zksdcjqa/xRBwdyFzIXWCcgP7XTgxjCgYsXOMKQl4=;\r\n",
|
||||
" b=NhoIMMAALoSgu5lKAR0+MUQunOWnU7wpF9ORUFtpxq9sGZDo9AX43AMhFemyM5W204jpFwMU6pm7AMR1nOYBdSYye4yUALtvT2nqbJBwSh7JeYu+z22t1RFKp7qQR1il8aSrkbZuNMFHYuSEwW76QtKwcNqP4bQOzS9CzgQp0ABu8qwYPBr/EypykPTfqjtyN+ywrfdqjjGOzTpRGolH0hc3CrAETNjjHbNBgKgucXmXTN7hMRdzqWjeFPxizXwouwNAavFClPG0l33gXVArFWn+CkgA84G/s4zuJiF7QPZR87Pu4pw/vIlSXxH4a42W3tT19v9iBTH7X7ldYegtmQ==\r\n",
|
||||
"\r\n",
|
||||
"test\r\n",
|
||||
"\r\n",
|
||||
"test \ttest\r\n",
|
||||
"\r\n",
|
||||
"\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_signature_rsa_relaxed() {
|
||||
let mut message = test_message();
|
||||
let signing_key = DkimSigningKey::new(KEY_RSA, DkimSigningAlgorithm::Rsa).unwrap();
|
||||
dkim_sign_fixed_time(
|
||||
&mut message,
|
||||
&DkimConfig::new(
|
||||
"dkimtest".to_owned(),
|
||||
"example.org".to_owned(),
|
||||
signing_key,
|
||||
vec![
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
],
|
||||
DkimCanonicalization {
|
||||
header: DkimCanonicalizationType::Relaxed,
|
||||
body: DkimCanonicalizationType::Relaxed,
|
||||
},
|
||||
),
|
||||
std::time::UNIX_EPOCH,
|
||||
);
|
||||
let signed = message.formatted();
|
||||
let signed = std::str::from_utf8(&signed).unwrap();
|
||||
println!("{signed}");
|
||||
assert_eq!(
|
||||
signed,
|
||||
std::concat!(
|
||||
"From: =?utf-8?b?VGVzdCBPJ0xlYXJ5?= <test+ezrz@example.net>\r\n",
|
||||
"To: Test2 <test2@example.org>\r\n",
|
||||
"Date: Thu, 01 Jan 1970 00:00:00 +0000\r\n",
|
||||
"Test: test test very very long with spaces and extra spaces \twill be\r\n",
|
||||
" folded to several lines \r\n","Subject: Test with utf-8 =?utf-8?b?w6s=?=\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"DKIM-Signature: v=1; a=rsa-sha256; d=example.org; s=dkimtest;\r\n",
|
||||
" c=relaxed/relaxed; q=dns/txt; t=0; h=date:from:subject:to;\r\n",
|
||||
" bh=qN8je6qJgWFGSnN2MycC/XKPbN6BOrMJyAX2h4m19Ss=;\r\n",
|
||||
" b=YaVfmH8dbGEywoLJ4uhbvYqDyQG1UGKFH3PE7zXGgk+YFxUgkwWjoA3aQupDNQtfTjfUsNe0dnrjyZP+ylnESpZBpbCIf5/n3FEh6j3RQthqNbQblcfH/U8mazTuRbVjYBbTZQDaQCMPTz+8D+ZQfXo2oq6dGzTuGvmuYft0CVsq/BIp/EkhZHqiphDeVJSHD4iKW8+L2XwEWThoY92xOYc1G0TtBwz2UJgtiHX2YulH/kRBHeK3dKn9RTNVL3VZ+9ZrnFwIhET9TPGtU2I+q0EMSWF9H9bTrASMgW/U+E0VM2btqJlrTU6rQ7wlQeHdwecLnzXcyhCUInF1+veMNw==\r\n",
|
||||
"\r\n",
|
||||
"test\r\n",
|
||||
"\r\n",
|
||||
"test \ttest\r\n",
|
||||
"\r\n",
|
||||
"\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
116
src/message/header/content.rs
Normal file
116
src/message/header/content.rs
Normal file
@@ -0,0 +1,116 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Transfer-Encoding` of the body
|
||||
///
|
||||
/// The `Message` builder takes care of choosing the most
|
||||
/// efficient encoding based on the chosen body, so in most
|
||||
/// use-caches this header shouldn't be set manually.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[derive(Default)]
|
||||
pub enum ContentTransferEncoding {
|
||||
/// ASCII
|
||||
SevenBit,
|
||||
/// Quoted-Printable encoding
|
||||
QuotedPrintable,
|
||||
/// base64 encoding
|
||||
#[default]
|
||||
Base64,
|
||||
/// Requires `8BITMIME`
|
||||
EightBit,
|
||||
/// Binary data
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(s.parse()?)
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let val = self.to_string();
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTransferEncoding {
|
||||
fn fmt(&self, f: &mut FmtFormatter<'_>) -> FmtResult {
|
||||
f.write_str(match *self {
|
||||
Self::SevenBit => "7bit",
|
||||
Self::QuotedPrintable => "quoted-printable",
|
||||
Self::Base64 => "base64",
|
||||
Self::EightBit => "8bit",
|
||||
Self::Binary => "binary",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentTransferEncoding {
|
||||
type Err = String;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"7bit" => Ok(Self::SevenBit),
|
||||
"quoted-printable" => Ok(Self::QuotedPrintable),
|
||||
"base64" => Ok(Self::Base64),
|
||||
"8bit" => Ok(Self::EightBit),
|
||||
"binary" => Ok(Self::Binary),
|
||||
_ => Err(s.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentTransferEncoding;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: 7bit\r\n");
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(headers.to_string(), "Content-Transfer-Encoding: base64\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"7bit".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(ContentTransferEncoding::SevenBit)
|
||||
);
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"base64".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
127
src/message/header/content_disposition.rs
Normal file
127
src/message/header/content_disposition.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Disposition` of an attachment
|
||||
///
|
||||
/// Defined in [RFC2183](https://tools.ietf.org/html/rfc2183)
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ContentDisposition(HeaderValue);
|
||||
|
||||
impl ContentDisposition {
|
||||
/// An attachment which should be displayed inline into the message
|
||||
pub fn inline() -> Self {
|
||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||
Self::name(),
|
||||
"inline".to_owned(),
|
||||
"inline".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
/// An attachment which should be displayed inline into the message, but that also
|
||||
/// species the filename in case it is downloaded
|
||||
pub fn inline_with_name(file_name: &str) -> Self {
|
||||
Self::with_name("inline", file_name)
|
||||
}
|
||||
|
||||
/// An attachment which is separate from the body of the message, and can be downloaded separately
|
||||
pub fn attachment(file_name: &str) -> Self {
|
||||
Self::with_name("attachment", file_name)
|
||||
}
|
||||
|
||||
fn with_name(kind: &str, file_name: &str) -> Self {
|
||||
let raw_value = format!("{kind}; filename=\"{file_name}\"");
|
||||
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = "Content-Disposition: ".len();
|
||||
{
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
w.write_str(kind).expect("writing `kind` returned an error");
|
||||
w.write_char(';').expect("writing `;` returned an error");
|
||||
w.space();
|
||||
|
||||
email_encoding::headers::rfc2231::encode("filename", file_name, &mut w)
|
||||
.expect("some Write implementation returned an error");
|
||||
}
|
||||
|
||||
Self(HeaderValue::dangerous_new_pre_encoded(
|
||||
Self::name(),
|
||||
raw_value,
|
||||
encoded_value,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentDisposition {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Disposition")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
match (s.split_once(';'), s) {
|
||||
(_, "inline") => Ok(Self::inline()),
|
||||
(Some((kind @ ("inline" | "attachment"), file_name)), _) => file_name
|
||||
.split_once(" filename=\"")
|
||||
.and_then(|(_, file_name)| file_name.strip_suffix('"'))
|
||||
.map(|file_name| Self::with_name(kind, file_name))
|
||||
.ok_or_else(|| "Unsupported ContentDisposition value".into()),
|
||||
_ => Err("Unsupported ContentDisposition value".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
self.0.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentDisposition;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentDisposition::inline());
|
||||
|
||||
assert_eq!(format!("{headers}"), "Content-Disposition: inline\r\n");
|
||||
|
||||
headers.set(ContentDisposition::attachment("something.txt"));
|
||||
|
||||
assert_eq!(
|
||||
format!("{headers}"),
|
||||
"Content-Disposition: attachment; filename=\"something.txt\"\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_disposition() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"inline".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::inline())
|
||||
);
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Disposition"),
|
||||
"attachment; filename=\"something.txt\"".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentDisposition>(),
|
||||
Some(ContentDisposition::attachment("something.txt"))
|
||||
);
|
||||
}
|
||||
}
|
||||
193
src/message/header/content_type.rs
Normal file
193
src/message/header/content_type.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// `Content-Type` of the body
|
||||
///
|
||||
/// This struct can represent any valid [MIME type], which can be parsed via
|
||||
/// [`ContentType::parse`]. Constants are provided for the most-used mime-types.
|
||||
///
|
||||
/// Defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-5)
|
||||
///
|
||||
/// [MIME type]: https://www.iana.org/assignments/media-types/media-types.xhtml
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContentType(Mime);
|
||||
|
||||
impl ContentType {
|
||||
/// A `ContentType` of type `text/plain; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded plain text.
|
||||
pub const TEXT_PLAIN: ContentType = Self::from_mime(mime::TEXT_PLAIN_UTF_8);
|
||||
|
||||
/// A `ContentType` of type `text/html; charset=utf-8`
|
||||
///
|
||||
/// Indicates that the body is in utf-8 encoded html.
|
||||
pub const TEXT_HTML: ContentType = Self::from_mime(mime::TEXT_HTML_UTF_8);
|
||||
|
||||
/// Parse `s` into `ContentType`
|
||||
pub fn parse(s: &str) -> Result<ContentType, ContentTypeErr> {
|
||||
Ok(Self::from_mime(s.parse().map_err(ContentTypeErr)?))
|
||||
}
|
||||
|
||||
pub(crate) const fn from_mime(mime: Mime) -> Self {
|
||||
Self(mime)
|
||||
}
|
||||
|
||||
pub(crate) fn as_ref(&self) -> &Mime {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentType {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Content-Type")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.parse()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentType {
|
||||
type Err = ContentTypeErr;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::parse(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "mime03")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "mime03")))]
|
||||
impl From<Mime> for ContentType {
|
||||
fn from(mime: Mime) -> Self {
|
||||
Self::from_mime(mime)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error occurred while trying to [`ContentType::parse`].
|
||||
#[derive(Debug)]
|
||||
pub struct ContentTypeErr(mime::FromStrError);
|
||||
|
||||
impl StdError for ContentTypeErr {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
Some(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentTypeErr {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Serialization and Deserialization --
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde {
|
||||
use std::fmt;
|
||||
|
||||
use serde::{
|
||||
de::{self, Deserialize, Deserializer, Visitor},
|
||||
ser::{Serialize, Serializer},
|
||||
};
|
||||
|
||||
use super::ContentType;
|
||||
|
||||
impl Serialize for ContentType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_newtype_struct("ContentType", &format!("{}", &self.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ContentType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct ContentTypeVisitor;
|
||||
|
||||
impl Visitor<'_> for ContentTypeVisitor {
|
||||
type Value = ContentType;
|
||||
|
||||
// The error message which states what the Visitor expects to
|
||||
// receive
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("a ContentType string like `text/plain`")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, mime: &str) -> Result<ContentType, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match ContentType::parse(mime) {
|
||||
Ok(content_type) => Ok(content_type),
|
||||
Err(_) => Err(E::custom(format!(
|
||||
"Couldn't parse the following MIME-Type: {mime}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(ContentTypeVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::ContentType;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentType::TEXT_PLAIN);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/plain; charset=utf-8\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentType::TEXT_HTML);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_type() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/plain; charset=utf-8".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_PLAIN));
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Type"),
|
||||
"text/html; charset=utf-8".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<ContentType>(), Some(ContentType::TEXT_HTML));
|
||||
}
|
||||
}
|
||||
135
src/message/header/date.rs
Normal file
135
src/message/header/date.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use httpdate::HttpDate;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
/// Message `Date` header
|
||||
///
|
||||
/// Defined in [RFC2822](https://tools.ietf.org/html/rfc2822#section-3.3)
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Date(HttpDate);
|
||||
|
||||
impl Date {
|
||||
/// Build a `Date` from [`SystemTime`]
|
||||
pub fn new(st: SystemTime) -> Self {
|
||||
Self(st.into())
|
||||
}
|
||||
|
||||
/// Get the current date
|
||||
///
|
||||
/// Shortcut for `Date::new(SystemTime::now())`
|
||||
pub fn now() -> Self {
|
||||
Self::new(crate::time::now())
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for Date {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("Date")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = String::from(s);
|
||||
if s.ends_with("+0000") {
|
||||
// The httpdate crate expects the `Date` to end in ` GMT`, but email
|
||||
// uses `+0000` to indicate UTC, so we crudely fix this issue here.
|
||||
|
||||
s.truncate(s.len() - "+0000".len());
|
||||
s.push_str("GMT");
|
||||
}
|
||||
|
||||
Ok(Self(s.parse::<HttpDate>()?))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut val = self.0.to_string();
|
||||
if val.ends_with(" GMT") {
|
||||
// The httpdate crate always appends ` GMT` to the end of the string,
|
||||
// but this is considered an obsolete date format for email
|
||||
// https://tools.ietf.org/html/rfc2822#appendix-A.6.2,
|
||||
// so we replace `GMT` with `+0000`
|
||||
val.truncate(val.len() - "GMT".len());
|
||||
val.push_str("+0000");
|
||||
}
|
||||
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemTime> for Date {
|
||||
fn from(st: SystemTime) -> Self {
|
||||
Self::new(st)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Date> for SystemTime {
|
||||
fn from(this: Date) -> SystemTime {
|
||||
this.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Date;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n".to_owned()
|
||||
);
|
||||
|
||||
// Tue, 15 Nov 1994 08:12:32 GMT
|
||||
headers.set(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Date: Tue, 15 Nov 1994 08:12:32 +0000\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_date() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:31 +0000".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887151),
|
||||
))
|
||||
);
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Date"),
|
||||
"Tue, 15 Nov 1994 08:12:32 +0000".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Date>(),
|
||||
Some(Date::from(
|
||||
SystemTime::UNIX_EPOCH + Duration::from_secs(784887152),
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
359
src/message/header/mailbox.rs
Normal file
359
src/message/header/mailbox.rs
Normal file
@@ -0,0 +1,359 @@
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::{
|
||||
message::mailbox::{Mailbox, Mailboxes},
|
||||
BoxError,
|
||||
};
|
||||
|
||||
/// Header which can contains multiple mailboxes
|
||||
pub trait MailboxesHeader {
|
||||
fn join_mailboxes(&mut self, other: Self);
|
||||
}
|
||||
|
||||
macro_rules! mailbox_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailbox = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = $header_name.len() + ": ".len();
|
||||
{
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
self.0.encode(&mut w).expect("writing `Mailbox` returned an error");
|
||||
}
|
||||
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailbox> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailbox: Mailbox) -> Self {
|
||||
Self(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailbox {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailbox {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(pub(crate) Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mailbox: Mailboxes = s.parse()?;
|
||||
Ok(Self(mailbox))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let mut encoded_value = String::new();
|
||||
let line_len = $header_name.len() + ": ".len();
|
||||
{
|
||||
let mut w = EmailWriter::new(&mut encoded_value, line_len, 0, false);
|
||||
self.0.encode(&mut w).expect("writing `Mailboxes` returned an error");
|
||||
}
|
||||
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), self.0.to_string(), encoded_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<Mailboxes> for $type_name {
|
||||
#[inline]
|
||||
fn from(mailboxes: Mailboxes) -> Self {
|
||||
Self(mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<$type_name> for Mailboxes {
|
||||
#[inline]
|
||||
fn from(this: $type_name) -> Mailboxes {
|
||||
this.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mailbox_header! {
|
||||
/**
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`] associated with sender.
|
||||
|
||||
```no_test
|
||||
header::Sender("Mr. Sender <sender@example.com>".parse().unwrap())
|
||||
```
|
||||
*/
|
||||
(Sender, "Sender")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`From` header
|
||||
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`].
|
||||
|
||||
*/
|
||||
(Bcc, "Bcc")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{From, Mailbox, Mailboxes};
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_single_without_name() {
|
||||
let from = Mailboxes::new().with("kayo@example.com".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(headers.to_string(), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_name() {
|
||||
let from = Mailboxes::new().with("Kayo <kayo@example.com>".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(headers.to_string(), "From: Kayo <kayo@example.com>\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_without_name() {
|
||||
let from = Mailboxes::new()
|
||||
.with("kayo@example.com".parse().unwrap())
|
||||
.with("pony@domain.tld".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_with_name() {
|
||||
let from = vec![
|
||||
"Kayo <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"From: Kayo <kayo@example.com>, \"Pony P.\" <pony@domain.tld>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_utf8_name() {
|
||||
let from = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"From: =?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_without_name() {
|
||||
let from = vec!["kayo@example.com".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_name() {
|
||||
let from = vec!["K. <kayo@example.com>".parse().unwrap()].into();
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"K. <kayo@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_without_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
"pony@domain.tld".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"kayo@example.com, pony@domain.tld".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"K. <kayo@example.com>, Pony P. <pony@domain.tld>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name_containing_comma() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"\"Test, test\" <1@example.com>".parse().unwrap(),
|
||||
"\"Test2, test2\" <2@example.com>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"\"Test, test\" <1@example.com>, \"Test2, test2\" <2@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name_containing_double_quotes() {
|
||||
let from: Vec<Mailbox> = vec![
|
||||
"\"Test, test\" <1@example.com>".parse().unwrap(),
|
||||
"\"Test2, \"test2\"\" <2@example.com>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"\"Test, test\" <1@example.com>, \"Test2, \"test2\"\" <2@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_multi_with_name_containing_comma_last_broken() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"\"Test, test\" <1@example.com>, \"Test2, test2\"".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<From>(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_angle_bracket() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("<3".into()), "i@love.example".parse().unwrap())
|
||||
),
|
||||
r#""<3" <i@love.example>"#
|
||||
);
|
||||
}
|
||||
}
|
||||
754
src/message/header/mod.rs
Normal file
754
src/message/header/mod.rs
Normal file
@@ -0,0 +1,754 @@
|
||||
//! Headers widely used in email messages
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error,
|
||||
fmt::{self, Display, Formatter, Write},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
pub use self::{
|
||||
content::*,
|
||||
content_disposition::ContentDisposition,
|
||||
content_type::{ContentType, ContentTypeErr},
|
||||
date::Date,
|
||||
mailbox::*,
|
||||
special::*,
|
||||
textual::*,
|
||||
};
|
||||
use crate::BoxError;
|
||||
|
||||
mod content;
|
||||
mod content_disposition;
|
||||
mod content_type;
|
||||
mod date;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
/// Represents an email header
|
||||
///
|
||||
/// Email header as defined in [RFC5322](https://datatracker.ietf.org/doc/html/rfc5322) and extensions.
|
||||
pub trait Header: Clone {
|
||||
fn name() -> HeaderName;
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError>;
|
||||
|
||||
fn display(&self) -> HeaderValue;
|
||||
}
|
||||
|
||||
/// A set of email headers
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Headers {
|
||||
headers: Vec<HeaderValue>,
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
/// Create an empty `Headers`
|
||||
///
|
||||
/// This function does not allocate.
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
headers: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty `Headers` with a pre-allocated capacity
|
||||
///
|
||||
/// Pre-allocates a capacity of at least `capacity`.
|
||||
#[inline]
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
headers: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a copy of a `Header` present in `Headers`
|
||||
///
|
||||
/// Returns `None` if `Header` isn't present in `Headers`.
|
||||
pub fn get<H: Header>(&self) -> Option<H> {
|
||||
self.get_raw(&H::name())
|
||||
.and_then(|raw_value| H::parse(raw_value).ok())
|
||||
}
|
||||
|
||||
/// Sets `Header` into `Headers`, overriding `Header` if it
|
||||
/// was already present in `Headers`
|
||||
pub fn set<H: Header>(&mut self, header: H) {
|
||||
self.insert_raw(header.display());
|
||||
}
|
||||
|
||||
/// Remove `Header` from `Headers`, returning it
|
||||
///
|
||||
/// Returns `None` if `Header` isn't in `Headers`.
|
||||
pub fn remove<H: Header>(&mut self) -> Option<H> {
|
||||
self.remove_raw(&H::name())
|
||||
.and_then(|value| H::parse(&value.raw_value).ok())
|
||||
}
|
||||
|
||||
/// Clears `Headers`, removing all headers from it
|
||||
///
|
||||
/// Any pre-allocated capacity is left untouched.
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.headers.clear();
|
||||
}
|
||||
|
||||
/// Returns a reference to the raw value of header `name`
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn get_raw(&self, name: &str) -> Option<&str> {
|
||||
self.find_header(name).map(|value| value.raw_value.as_str())
|
||||
}
|
||||
|
||||
/// Inserts a raw header into `Headers`, overriding `value` if it
|
||||
/// was already present in `Headers`.
|
||||
pub fn insert_raw(&mut self, value: HeaderValue) {
|
||||
match self.find_header_mut(&value.name) {
|
||||
Some(current_value) => {
|
||||
*current_value = value;
|
||||
}
|
||||
None => {
|
||||
self.headers.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a raw header from `Headers`, returning it
|
||||
///
|
||||
/// Returns `None` if `name` isn't present in `Headers`.
|
||||
pub fn remove_raw(&mut self, name: &str) -> Option<HeaderValue> {
|
||||
self.find_header_index(name).map(|i| self.headers.remove(i))
|
||||
}
|
||||
|
||||
pub(crate) fn find_header(&self, name: &str) -> Option<&HeaderValue> {
|
||||
self.headers.iter().find(|value| name == value.name)
|
||||
}
|
||||
|
||||
fn find_header_mut(&mut self, name: &str) -> Option<&mut HeaderValue> {
|
||||
self.headers.iter_mut().find(|value| name == value.name)
|
||||
}
|
||||
|
||||
fn find_header_index(&self, name: &str) -> Option<usize> {
|
||||
self.headers
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_i, value)| name == value.name)
|
||||
.map(|(i, _)| i)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Headers {
|
||||
/// Formats `Headers`, ready to put them into an email
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for value in &self.headers {
|
||||
f.write_str(&value.name)?;
|
||||
f.write_str(": ")?;
|
||||
f.write_str(&value.encoded_value)?;
|
||||
f.write_str("\r\n")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A possible error when converting a `HeaderName` from another type.
|
||||
// comes from `http` crate
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub struct InvalidHeaderName;
|
||||
|
||||
impl fmt::Display for InvalidHeaderName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("invalid header name")
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InvalidHeaderName {}
|
||||
|
||||
/// A valid header name
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HeaderName(Cow<'static, str>);
|
||||
|
||||
impl HeaderName {
|
||||
/// Creates a new header name
|
||||
pub fn new_from_ascii(ascii: String) -> Result<Self, InvalidHeaderName> {
|
||||
if !ascii.is_empty() && ascii.len() <= 76 && ascii.is_ascii() && !ascii.contains([':', ' '])
|
||||
{
|
||||
Ok(Self(Cow::Owned(ascii)))
|
||||
} else {
|
||||
Err(InvalidHeaderName)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new header name, panics on invalid name
|
||||
pub const fn new_from_ascii_str(ascii: &'static str) -> Self {
|
||||
macro_rules! static_assert {
|
||||
($condition:expr) => {
|
||||
let _ = [()][(!($condition)) as usize];
|
||||
};
|
||||
}
|
||||
|
||||
static_assert!(!ascii.is_empty());
|
||||
static_assert!(ascii.len() <= 76);
|
||||
|
||||
let bytes = ascii.as_bytes();
|
||||
let mut i = 0;
|
||||
while i < bytes.len() {
|
||||
static_assert!(bytes[i].is_ascii());
|
||||
static_assert!(bytes[i] != b' ');
|
||||
static_assert!(bytes[i] != b':');
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
Self(Cow::Borrowed(ascii))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for HeaderName {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for HeaderName {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
let s: &str = self.as_ref();
|
||||
s.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for HeaderName {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for HeaderName {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
self.eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<&str> for HeaderName {
|
||||
fn eq(&self, other: &&str) -> bool {
|
||||
self.eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<HeaderName> for &str {
|
||||
fn eq(&self, other: &HeaderName) -> bool {
|
||||
self.eq_ignore_ascii_case(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A safe for use header value
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HeaderValue {
|
||||
name: HeaderName,
|
||||
raw_value: String,
|
||||
encoded_value: String,
|
||||
}
|
||||
|
||||
impl HeaderValue {
|
||||
/// Construct a new `HeaderValue` and encode it
|
||||
///
|
||||
/// Takes the header `name` and the `raw_value` and encodes
|
||||
/// it via `RFC2047` and line folds it.
|
||||
///
|
||||
/// [`RFC2047`]: https://datatracker.ietf.org/doc/html/rfc2047
|
||||
pub fn new(name: HeaderName, raw_value: String) -> Self {
|
||||
let mut encoded_value = String::with_capacity(raw_value.len());
|
||||
HeaderValueEncoder::encode(&name, &raw_value, &mut encoded_value).unwrap();
|
||||
|
||||
Self {
|
||||
name,
|
||||
raw_value,
|
||||
encoded_value,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a new `HeaderValue` using a pre-encoded header value
|
||||
///
|
||||
/// This method is _extremely_ dangerous as it opens up
|
||||
/// the encoder to header injection attacks, but is sometimes
|
||||
/// acceptable for use if `encoded_value` contains only ascii
|
||||
/// printable characters and is already line folded.
|
||||
///
|
||||
/// When in doubt, use [`HeaderValue::new`].
|
||||
pub fn dangerous_new_pre_encoded(
|
||||
name: HeaderName,
|
||||
raw_value: String,
|
||||
encoded_value: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
raw_value,
|
||||
encoded_value,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dkim")]
|
||||
pub(crate) fn get_raw(&self) -> &str {
|
||||
&self.raw_value
|
||||
}
|
||||
|
||||
#[cfg(feature = "dkim")]
|
||||
pub(crate) fn get_encoded(&self) -> &str {
|
||||
&self.encoded_value
|
||||
}
|
||||
}
|
||||
|
||||
/// [RFC 1522](https://tools.ietf.org/html/rfc1522) header value encoder
|
||||
struct HeaderValueEncoder<'a> {
|
||||
writer: EmailWriter<'a>,
|
||||
encode_buf: String,
|
||||
}
|
||||
|
||||
impl<'a> HeaderValueEncoder<'a> {
|
||||
fn encode(name: &str, value: &'a str, f: &'a mut impl fmt::Write) -> fmt::Result {
|
||||
let encoder = Self::new(name, f);
|
||||
encoder.format(value.split_inclusive(' '))
|
||||
}
|
||||
|
||||
fn new(name: &str, writer: &'a mut dyn Write) -> Self {
|
||||
let line_len = name.len() + ": ".len();
|
||||
let writer = EmailWriter::new(writer, line_len, 0, false);
|
||||
|
||||
Self {
|
||||
writer,
|
||||
encode_buf: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn format(mut self, words_iter: impl Iterator<Item = &'a str>) -> fmt::Result {
|
||||
for next_word in words_iter {
|
||||
let allowed = allowed_str(next_word);
|
||||
|
||||
if allowed {
|
||||
// This word only contains allowed characters
|
||||
|
||||
// the next word is allowed, but we may have accumulated some words to encode
|
||||
self.flush_encode_buf()?;
|
||||
|
||||
self.writer.folding().write_str(next_word)?;
|
||||
} else {
|
||||
// This word contains unallowed characters
|
||||
self.encode_buf.push_str(next_word);
|
||||
}
|
||||
}
|
||||
|
||||
self.flush_encode_buf()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn flush_encode_buf(&mut self) -> fmt::Result {
|
||||
if self.encode_buf.is_empty() {
|
||||
// nothing to encode
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let prefix = self.encode_buf.trim_end_matches(' ');
|
||||
email_encoding::headers::rfc2047::encode(prefix, &mut self.writer)?;
|
||||
|
||||
// TODO: add a better API for doing this in email-encoding
|
||||
let spaces = self.encode_buf.len() - prefix.len();
|
||||
for _ in 0..spaces {
|
||||
self.writer.space();
|
||||
}
|
||||
|
||||
self.encode_buf.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn allowed_str(s: &str) -> bool {
|
||||
s.bytes().all(allowed_char)
|
||||
}
|
||||
|
||||
const fn allowed_char(c: u8) -> bool {
|
||||
c >= 1 && c <= 9 || c == 11 || c == 12 || c >= 14 && c <= 127
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{HeaderName, HeaderValue, Headers, To};
|
||||
use crate::message::Mailboxes;
|
||||
|
||||
#[test]
|
||||
fn valid_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_ascii_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("🌎")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaces_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From ")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colons_in_headername() {
|
||||
assert!(HeaderName::new_from_ascii(String::from("From:")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_headername() {
|
||||
assert!(HeaderName::new_from_ascii("".to_owned()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn const_valid_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_non_ascii_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("🌎");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_spaces_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_colons_in_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("From:");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn const_empty_headername() {
|
||||
let _ = HeaderName::new_from_ascii_str("");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_headername_eq() {
|
||||
assert_eq!(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("From")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_str_eq() {
|
||||
assert_eq!(HeaderName::new_from_ascii_str("From"), "From");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_headername_eq() {
|
||||
assert_eq!("From", HeaderName::new_from_ascii_str("From"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_headername_eq_case_insensitive() {
|
||||
assert_eq!(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("from")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_str_eq_case_insensitive() {
|
||||
assert_eq!(HeaderName::new_from_ascii_str("From"), "from");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_headername_eq_case_insensitive() {
|
||||
assert_eq!("from", HeaderName::new_from_ascii_str("From"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_headername_ne() {
|
||||
assert_ne!(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
HeaderName::new_from_ascii_str("To")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn headername_str_ne() {
|
||||
assert_ne!(HeaderName::new_from_ascii_str("From"), "To");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn str_headername_ne() {
|
||||
assert_ne!("From", HeaderName::new_from_ascii_str("To"));
|
||||
}
|
||||
|
||||
// names taken randomly from https://it.wikipedia.org/wiki/Pinco_Pallino
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"John Doe <example@example.com>, Jean Dupont <jean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: John Doe <example@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand <jemand@example.com>, Jean Dupont <jean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"To: Ascii <example@example.com>, John Doe <johndoe@example.com, John Smith\r\n",
|
||||
" <johnsmith@example.com>, Pinco Pallino <pincopallino@example.com>, Jemand\r\n",
|
||||
" <jemand@example.com>, Jean Dupont <jean@example.com>\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this\r\n",
|
||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||
" guess that's it!\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_very_long_line() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut! I don't know".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello!\r\n",
|
||||
" IGuessTheLastLineWasntLongEnoughSoLetsTryAgainShallWeWhatDoYouThinkItsGoingToHappenIGuessWereAboutToFindOut!\r\n",
|
||||
" I don't know\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_ascii_with_folding_giant_word() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz".to_owned()
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: 1abcdefghijklmnopqrstuvwxyz2abcdefghijklmnopqrstuvwxyz3abcdefghijklmnopqrstuvwxyz4abcdefghijklmnopqrstuvwxyz5abcdefghijklmnopqrstuvwxyz6abcdefghijklmnopqrstuvwxyz\r\n",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"Seán <sean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?U2XDoW4=?= <sean@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_emoji() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌎 <world@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"To: =?utf-8?b?8J+Mjg==?= <world@example.com>\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_with_folding() {
|
||||
let mut headers = Headers::new();
|
||||
let to = To::from(Mailboxes::from_iter([
|
||||
"🌍 <world@example.com>".parse().unwrap(),
|
||||
"🦆 Everywhere <ducks@example.com>".parse().unwrap(),
|
||||
"Иванов Иван Иванович <ivanov@example.com>".parse().unwrap(),
|
||||
"Jānis Bērziņš <janis@example.com>".parse().unwrap(),
|
||||
"Seán Ó Rudaí <sean@example.com>".parse().unwrap(),
|
||||
]));
|
||||
headers.set(to);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhiBFdmVyeXdo?=\r\n",
|
||||
" =?utf-8?b?ZXJl?= <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LI=?=\r\n",
|
||||
" =?utf-8?b?0LDQvSDQmNCy0LDQvdC+0LLQuNGH?= <ivanov@example.com>,\r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_special_with_folding_raw() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_slice_on_char_boundary_bug() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳🥳".to_owned(),)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz?=\r\n",
|
||||
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+ls/CfpbM=?=\r\n",
|
||||
" =?utf-8?b?8J+ls/CfpbPwn6Wz8J+ls/CfpbPwn6Wz8J+lsw==?=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_bad_stuff() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! \r\n This is \" bad \0. 👋".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: Hello! =?utf-8?b?DQo=?= This is \" bad =?utf-8?b?AC4g8J+Riw==?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_everything() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Hello! This is lettre, and this IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I guess that's it!".to_owned()
|
||||
)
|
||||
);
|
||||
headers.insert_raw(
|
||||
HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("To"),
|
||||
"🌍 <world@example.com>, 🦆 Everywhere <ducks@example.com>, Иванов Иван Иванович <ivanov@example.com>, Jānis Bērziņš <janis@example.com>, Seán Ó Rudaí <sean@example.com>".to_owned(),
|
||||
)
|
||||
);
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("From"),
|
||||
"Someone <somewhere@example.com>".to_owned(),
|
||||
));
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Content-Transfer-Encoding"),
|
||||
"quoted-printable".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: Hello! This is lettre, and this\r\n",
|
||||
" IsAVeryLongLineDoYouKnowWhatsGoingToHappenIGuessWeAreGoingToFindOut. Ok I\r\n",
|
||||
" guess that's it!\r\n",
|
||||
"To: =?utf-8?b?8J+MjQ==?= <world@example.com>, =?utf-8?b?8J+mhg==?=\r\n",
|
||||
" Everywhere <ducks@example.com>, =?utf-8?b?0JjQstCw0L3QvtCyINCY0LLQsNC9?=\r\n",
|
||||
" =?utf-8?b?INCY0LLQsNC90L7QstC40Yc=?= <ivanov@example.com>,\r\n",
|
||||
" =?utf-8?b?SsSBbmlzIELEk3J6acWGxaE=?= <janis@example.com>, =?utf-8?b?U2U=?=\r\n",
|
||||
" =?utf-8?b?w6FuIMOTIFJ1ZGHDrQ==?= <sean@example.com>\r\n",
|
||||
"From: Someone <somewhere@example.com>\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issue_653() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"+仮名 :a;go; ;;;;;s;;;;;;;;;;;;;;;;fffeinmjgggggggggfっ".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
concat!(
|
||||
"Subject: =?utf-8?b?77yL5Luu5ZCN?= :a;go; =?utf-8?b?Ozs7OztzOzs7Ozs7Ozs7?=\r\n",
|
||||
" =?utf-8?b?Ozs7Ozs7O2ZmZmVpbm1qZ2dnZ2dnZ2dn772G44Gj?=\r\n",
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
106
src/message/header/special.rs
Normal file
106
src/message/header/special.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use crate::{
|
||||
message::header::{Header, HeaderName, HeaderValue},
|
||||
BoxError,
|
||||
};
|
||||
|
||||
/// Message format version, defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-4)
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct MimeVersion {
|
||||
major: u8,
|
||||
minor: u8,
|
||||
}
|
||||
|
||||
/// MIME version 1.0
|
||||
///
|
||||
/// Should be used in all MIME messages.
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion::new(1, 0);
|
||||
|
||||
impl MimeVersion {
|
||||
/// Build a new `MimeVersion` header
|
||||
pub const fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
|
||||
/// Get the `major` value of this `MimeVersion` header.
|
||||
#[inline]
|
||||
pub const fn major(self) -> u8 {
|
||||
self.major
|
||||
}
|
||||
|
||||
/// Get the `minor` value of this `MimeVersion` header.
|
||||
#[inline]
|
||||
pub const fn minor(self) -> u8 {
|
||||
self.minor
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str("MIME-Version")
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
let mut s = s.split('.');
|
||||
|
||||
let major = s
|
||||
.next()
|
||||
.expect("The first call to next for a Split<char> always succeeds");
|
||||
let minor = s
|
||||
.next()
|
||||
.ok_or_else(|| String::from("MIME-Version header doesn't contain '.'"))?;
|
||||
let major = major.parse()?;
|
||||
let minor = minor.parse()?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
let val = format!("{}.{}", self.major, self.minor);
|
||||
HeaderValue::dangerous_new_pre_encoded(Self::name(), val.clone(), val)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
fn default() -> Self {
|
||||
MIME_VERSION_1_0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(headers.to_string(), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"1.0".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(MIME_VERSION_1_0));
|
||||
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("MIME-Version"),
|
||||
"0.1".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
136
src/message/header/textual.rs
Normal file
136
src/message/header/textual.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use super::{Header, HeaderName, HeaderValue};
|
||||
use crate::BoxError;
|
||||
|
||||
macro_rules! text_header {
|
||||
($(#[$attr:meta])* Header($type_name: ident, $header_name: expr )) => {
|
||||
$(#[$attr])*
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct $type_name(String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn name() -> HeaderName {
|
||||
HeaderName::new_from_ascii_str($header_name)
|
||||
}
|
||||
|
||||
fn parse(s: &str) -> Result<Self, BoxError> {
|
||||
Ok(Self(s.into()))
|
||||
}
|
||||
|
||||
fn display(&self) -> HeaderValue {
|
||||
HeaderValue::new(Self::name(), self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for $type_name {
|
||||
#[inline]
|
||||
fn from(text: String) -> Self {
|
||||
Self(text)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for $type_name {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
text_header!(
|
||||
/// `Subject` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Subject, "Subject")
|
||||
);
|
||||
text_header!(
|
||||
/// `Comments` of the message, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Comments, "Comments")
|
||||
);
|
||||
text_header!(
|
||||
/// `Keywords` header. Should contain a comma-separated list of one or more
|
||||
/// words or quoted-strings, defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.5)
|
||||
Header(Keywords, "Keywords")
|
||||
);
|
||||
text_header!(
|
||||
/// `In-Reply-To` header. Contains one or more
|
||||
/// unique message identifiers,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(InReplyTo, "In-Reply-To")
|
||||
);
|
||||
text_header!(
|
||||
/// `References` header. Contains one or more
|
||||
/// unique message identifiers,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(References, "References")
|
||||
);
|
||||
text_header!(
|
||||
/// `Message-Id` header. Contains a unique message identifier,
|
||||
/// defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
Header(MessageId, "Message-ID")
|
||||
);
|
||||
text_header!(
|
||||
/// `User-Agent` header. Contains information about the client,
|
||||
/// defined in [draft-melnikov-email-user-agent-00](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00#section-3)
|
||||
Header(UserAgent, "User-Agent")
|
||||
);
|
||||
text_header! {
|
||||
/// `Content-Id` header,
|
||||
/// defined in [RFC2045](https://tools.ietf.org/html/rfc2045#section-7)
|
||||
Header(ContentId, "Content-ID")
|
||||
}
|
||||
text_header! {
|
||||
/// `Content-Location` header,
|
||||
/// defined in [RFC2110](https://tools.ietf.org/html/rfc2110#section-4.3)
|
||||
Header(ContentLocation, "Content-Location")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Subject;
|
||||
use crate::message::header::{HeaderName, HeaderValue, Headers};
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Sample subject".into()));
|
||||
|
||||
assert_eq!(headers.to_string(), "Subject: Sample subject\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_utf8_word() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Administratör".into()));
|
||||
|
||||
assert_eq!(
|
||||
headers.to_string(),
|
||||
"Subject: =?utf-8?b?QWRtaW5pc3RyYXTDtnI=?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.insert_raw(HeaderValue::new(
|
||||
HeaderName::new_from_ascii_str("Subject"),
|
||||
"Sample subject".to_owned(),
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
6
src/message/mailbox/mod.rs
Normal file
6
src/message/mailbox/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod parsers;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
pub use self::types::*;
|
||||
5
src/message/mailbox/parsers/mod.rs
Normal file
5
src/message/mailbox/parsers/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod rfc2234;
|
||||
mod rfc2822;
|
||||
mod rfc5336;
|
||||
|
||||
pub(crate) use rfc2822::{mailbox, mailbox_list};
|
||||
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
32
src/message/mailbox/parsers/rfc2234.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
//! Partial parsers implementation of [RFC2234]: Augmented BNF for
|
||||
//! Syntax Specifications: ABNF.
|
||||
//!
|
||||
//! [RFC2234]: https://datatracker.ietf.org/doc/html/rfc2234
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
// 6.1 Core Rules
|
||||
// https://datatracker.ietf.org/doc/html/rfc2234#section-6.1
|
||||
|
||||
// ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
|
||||
pub(super) fn alpha() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_alphabetic())
|
||||
}
|
||||
|
||||
// DIGIT = %x30-39
|
||||
// ; 0-9
|
||||
pub(super) fn digit() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.is_ascii_digit())
|
||||
}
|
||||
|
||||
// DQUOTE = %x22
|
||||
// ; " (Double Quote)
|
||||
pub(super) fn dquote() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('"')
|
||||
}
|
||||
|
||||
// WSP = SP / HTAB
|
||||
// ; white space
|
||||
pub(super) fn wsp() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((just(' '), just('\t')))
|
||||
}
|
||||
250
src/message/mailbox/parsers/rfc2822.rs
Normal file
250
src/message/mailbox/parsers/rfc2822.rs
Normal file
@@ -0,0 +1,250 @@
|
||||
//! Partial parsers implementation of [RFC2822]: Internet Message
|
||||
//! Format.
|
||||
//!
|
||||
//! [RFC2822]: https://datatracker.ietf.org/doc/html/rfc2822
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
use super::{rfc2234, rfc5336};
|
||||
|
||||
// 3.2.1. Primitive Tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
||||
|
||||
// NO-WS-CTL = %d1-8 / ; US-ASCII control characters
|
||||
// %d11 / ; that do not include the
|
||||
// %d12 / ; carriage return, line feed,
|
||||
// %d14-31 / ; and white space characters
|
||||
// %d127
|
||||
fn no_ws_ctl() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=8 | 11 | 12 | 14..=31 | 127))
|
||||
}
|
||||
|
||||
// text = %d1-9 / ; Characters excluding CR and LF
|
||||
// %d11 /
|
||||
// %d12 /
|
||||
// %d14-127 /
|
||||
// obs-text
|
||||
fn text() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c| matches!(u32::from(*c), 1..=9 | 11 | 12 | 14..=127))
|
||||
}
|
||||
|
||||
// 3.2.2. Quoted characters
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||
|
||||
// quoted-pair = ("\" text) / obs-qp
|
||||
fn quoted_pair() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
just('\\').ignore_then(text())
|
||||
}
|
||||
|
||||
// 3.2.3. Folding white space and comments
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.3
|
||||
|
||||
// FWS = ([*WSP CRLF] 1*WSP) / ; Folding white space
|
||||
// obs-FWS
|
||||
pub(super) fn fws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
rfc2234::wsp()
|
||||
.or_not()
|
||||
.then_ignore(rfc2234::wsp().ignored().repeated())
|
||||
}
|
||||
|
||||
// CFWS = *([FWS] comment) (([FWS] comment) / FWS)
|
||||
pub(super) fn cfws() -> impl Parser<char, Option<char>, Error = Cheap<char>> {
|
||||
// TODO: comment are not currently supported, so for now a cfws is
|
||||
// the same as a fws.
|
||||
fws()
|
||||
}
|
||||
|
||||
// 3.2.4. Atom
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
|
||||
|
||||
// atext = ALPHA / DIGIT / ; Any character except controls,
|
||||
// "!" / "#" / ; SP, and specials.
|
||||
// "$" / "%" / ; Used for atoms
|
||||
// "&" / "'" /
|
||||
// "*" / "+" /
|
||||
// "-" / "/" /
|
||||
// "=" / "?" /
|
||||
// "^" / "_" /
|
||||
// "`" / "{" /
|
||||
// "|" / "}" /
|
||||
// "~"
|
||||
pub(super) fn atext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
rfc2234::alpha(),
|
||||
rfc2234::digit(),
|
||||
filter(|c| {
|
||||
matches!(
|
||||
*c,
|
||||
'!' | '#'
|
||||
| '$'
|
||||
| '%'
|
||||
| '&'
|
||||
| '\''
|
||||
| '*'
|
||||
| '+'
|
||||
| '-'
|
||||
| '/'
|
||||
| '='
|
||||
| '?'
|
||||
| '^'
|
||||
| '_'
|
||||
| '`'
|
||||
| '{'
|
||||
| '|'
|
||||
| '}'
|
||||
| '~'
|
||||
)
|
||||
}),
|
||||
// also allow non ASCII UTF8 chars
|
||||
rfc5336::utf8_non_ascii(),
|
||||
))
|
||||
}
|
||||
|
||||
// atom = [CFWS] 1*atext [CFWS]
|
||||
pub(super) fn atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(atext().repeated().at_least(1))
|
||||
}
|
||||
|
||||
// dot-atom = [CFWS] dot-atom-text [CFWS]
|
||||
pub(super) fn dot_atom() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
cfws().chain(dot_atom_text())
|
||||
}
|
||||
|
||||
// dot-atom-text = 1*atext *("." 1*atext)
|
||||
pub(super) fn dot_atom_text() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atext().repeated().at_least(1).chain(
|
||||
just('.')
|
||||
.chain(atext().repeated().at_least(1))
|
||||
.repeated()
|
||||
.at_least(1)
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
|
||||
// 3.2.5. Quoted strings
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||
|
||||
// qtext = NO-WS-CTL / ; Non white space controls
|
||||
//
|
||||
// %d33 / ; The rest of the US-ASCII
|
||||
// %d35-91 / ; characters not including "\"
|
||||
// %d93-126 ; or the quote character
|
||||
fn qtext() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((
|
||||
filter(|c| matches!(u32::from(*c), 33 | 35..=91 | 93..=126)),
|
||||
no_ws_ctl(),
|
||||
))
|
||||
}
|
||||
|
||||
// qcontent = qtext / quoted-pair
|
||||
pub(super) fn qcontent() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
choice((qtext(), quoted_pair(), rfc5336::utf8_non_ascii()))
|
||||
}
|
||||
|
||||
// quoted-string = [CFWS]
|
||||
// DQUOTE *([FWS] qcontent) [FWS] DQUOTE
|
||||
// [CFWS]
|
||||
fn quoted_string() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
rfc2234::dquote()
|
||||
.ignore_then(fws().chain(qcontent()).repeated().flatten())
|
||||
.then_ignore(text::whitespace())
|
||||
.then_ignore(rfc2234::dquote())
|
||||
}
|
||||
|
||||
// 3.2.6. Miscellaneous tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||
|
||||
// word = atom / quoted-string
|
||||
fn word() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((quoted_string(), atom()))
|
||||
}
|
||||
|
||||
// phrase = 1*word / obs-phrase
|
||||
fn phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((obs_phrase(), word().repeated().at_least(1).flatten()))
|
||||
}
|
||||
|
||||
// 3.4. Address Specification
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||
|
||||
// mailbox = name-addr / addr-spec
|
||||
pub(crate) fn mailbox() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>>
|
||||
{
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.padded()
|
||||
.then_ignore(end())
|
||||
}
|
||||
|
||||
// name-addr = [display-name] angle-addr
|
||||
fn name_addr() -> impl Parser<char, (Option<String>, (String, String)), Error = Cheap<char>> {
|
||||
display_name().collect().or_not().then(angle_addr())
|
||||
}
|
||||
|
||||
// angle-addr = [CFWS] "<" addr-spec ">" [CFWS] / obs-angle-addr
|
||||
fn angle_addr() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
addr_spec()
|
||||
.delimited_by(just('<').ignored(), just('>').ignored())
|
||||
.padded()
|
||||
}
|
||||
|
||||
// display-name = phrase
|
||||
fn display_name() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
phrase()
|
||||
}
|
||||
|
||||
// mailbox-list = (mailbox *("," mailbox)) / obs-mbox-list
|
||||
pub(crate) fn mailbox_list(
|
||||
) -> impl Parser<char, Vec<(Option<String>, (String, String))>, Error = Cheap<char>> {
|
||||
choice((name_addr(), addr_spec().map(|addr| (None, addr))))
|
||||
.separated_by(just(',').padded())
|
||||
.then_ignore(end())
|
||||
}
|
||||
|
||||
// 3.4.1. Addr-spec specification
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.4.1
|
||||
|
||||
// addr-spec = local-part "@" domain
|
||||
pub(super) fn addr_spec() -> impl Parser<char, (String, String), Error = Cheap<char>> {
|
||||
local_part()
|
||||
.collect()
|
||||
.then_ignore(just('@'))
|
||||
.then(domain().collect())
|
||||
}
|
||||
|
||||
// local-part = dot-atom / quoted-string / obs-local-part
|
||||
pub(super) fn local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
choice((dot_atom(), quoted_string(), obs_local_part()))
|
||||
}
|
||||
|
||||
// domain = dot-atom / domain-literal / obs-domain
|
||||
pub(super) fn domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// NOTE: omitting domain-literal since it may never be used
|
||||
choice((dot_atom(), obs_domain()))
|
||||
}
|
||||
|
||||
// 4.1. Miscellaneous obsolete tokens
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.1
|
||||
|
||||
// obs-phrase = word *(word / "." / CFWS)
|
||||
fn obs_phrase() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
// NOTE: the CFWS is already captured by the word, no need to add
|
||||
// it there.
|
||||
word().chain(
|
||||
choice((word(), just('.').repeated().exactly(1)))
|
||||
.repeated()
|
||||
.flatten(),
|
||||
)
|
||||
}
|
||||
|
||||
// 4.4. Obsolete Addressing
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-4.4
|
||||
|
||||
// obs-local-part = word *("." word)
|
||||
pub(super) fn obs_local_part() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
word().chain(just('.').chain(word()).repeated().flatten())
|
||||
}
|
||||
|
||||
// obs-domain = atom *("." atom)
|
||||
pub(super) fn obs_domain() -> impl Parser<char, Vec<char>, Error = Cheap<char>> {
|
||||
atom().chain(just('.').chain(atom()).repeated().flatten())
|
||||
}
|
||||
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
17
src/message/mailbox/parsers/rfc5336.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
//! Partial parsers implementation of [RFC5336]: SMTP Extension for
|
||||
//! Internationalized Email Addresses.
|
||||
//!
|
||||
//! [RFC5336]: https://datatracker.ietf.org/doc/html/rfc5336
|
||||
|
||||
use chumsky::{error::Cheap, prelude::*};
|
||||
|
||||
// 3.3. Extended Mailbox Address Syntax
|
||||
// https://datatracker.ietf.org/doc/html/rfc5336#section-3.3
|
||||
|
||||
// UTF8-non-ascii = UTF8-2 / UTF8-3 / UTF8-4
|
||||
// UTF8-2 = <See Section 4 of RFC 3629>
|
||||
// UTF8-3 = <See Section 4 of RFC 3629>
|
||||
// UTF8-4 = <See Section 4 of RFC 3629>
|
||||
pub(super) fn utf8_non_ascii() -> impl Parser<char, char, Error = Cheap<char>> {
|
||||
filter(|c: &char| c.len_utf8() > 1)
|
||||
}
|
||||
219
src/message/mailbox/serde.rs
Normal file
219
src/message/mailbox/serde.rs
Normal file
@@ -0,0 +1,219 @@
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
|
||||
impl Serialize for Mailbox {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailbox {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
enum Field {
|
||||
Name,
|
||||
Email,
|
||||
}
|
||||
|
||||
const FIELDS: &[&str] = &["name", "email"];
|
||||
|
||||
impl<'de> Deserialize<'de> for Field {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct FieldVisitor;
|
||||
|
||||
impl Visitor<'_> for FieldVisitor {
|
||||
type Value = Field;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("'name' or 'email'")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Field, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
match value {
|
||||
"name" => Ok(Field::Name),
|
||||
"email" => Ok(Field::Email),
|
||||
_ => Err(DeError::unknown_field(value, FIELDS)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_identifier(FieldVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct MailboxVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxVisitor {
|
||||
type Value = Mailbox;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("mailbox string or object")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: MapAccess<'de>,
|
||||
{
|
||||
let mut name = None;
|
||||
let mut addr = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
Field::Name => {
|
||||
if name.is_some() {
|
||||
return Err(DeError::duplicate_field("name"));
|
||||
}
|
||||
name = Some(map.next_value()?);
|
||||
}
|
||||
Field::Email => {
|
||||
if addr.is_some() {
|
||||
return Err(DeError::duplicate_field("email"));
|
||||
}
|
||||
addr = Some(map.next_value()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
let addr = addr.ok_or_else(|| DeError::missing_field("email"))?;
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Mailboxes {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.collect_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Mailboxes {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct MailboxesVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for MailboxesVisitor {
|
||||
type Value = Mailboxes;
|
||||
|
||||
fn expecting(&self, formatter: &mut Formatter<'_>) -> FmtResult {
|
||||
formatter.write_str("mailboxes string or sequence")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: DeError,
|
||||
{
|
||||
s.parse().map_err(DeError::custom)
|
||||
}
|
||||
|
||||
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error>
|
||||
where
|
||||
V: SeqAccess<'de>,
|
||||
{
|
||||
let mut mboxes = Mailboxes::new();
|
||||
while let Some(mbox) = seq.next_element()? {
|
||||
mboxes.push(mbox);
|
||||
}
|
||||
Ok(mboxes)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(MailboxesVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::from_str;
|
||||
|
||||
use super::*;
|
||||
use crate::address::Address;
|
||||
|
||||
#[test]
|
||||
fn parse_address_string() {
|
||||
let m: Address = from_str(r#""kayo@example.com""#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_object() {
|
||||
let m: Address = from_str(r#"{ "user": "kayo", "domain": "example.com" }"#).unwrap();
|
||||
assert_eq!(m, "kayo@example.com".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_string() {
|
||||
let m: Mailbox = from_str(r#""Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_string() {
|
||||
let m: Mailbox = from_str(r#"{ "name": "Kai", "email": "kayo@example.com" }"#).unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailbox_object_address_object() {
|
||||
let m: Mailbox =
|
||||
from_str(r#"{ "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }"#)
|
||||
.unwrap();
|
||||
assert_eq!(m, "Kai <kayo@example.com>".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_string() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#""yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>""#).unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mailboxes_array() {
|
||||
let m: Mailboxes =
|
||||
from_str(r#"["yin@dtb.com", { "name": "Hei", "email": "hei@dtb.com" }, { "name": "Kai", "email": { "user": "kayo", "domain": "example.com" } }]"#)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m,
|
||||
"yin@dtb.com, Hei <hei@dtb.com>, Kai <kayo@example.com>"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
615
src/message/mailbox/types.rs
Normal file
615
src/message/mailbox/types.rs
Normal file
@@ -0,0 +1,615 @@
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
mem,
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chumsky::prelude::*;
|
||||
use email_encoding::headers::writer::EmailWriter;
|
||||
|
||||
use super::parsers;
|
||||
use crate::address::{Address, AddressError};
|
||||
|
||||
/// Represents an email address with an optional name for the sender/recipient.
|
||||
///
|
||||
/// This type contains email address and the sender/recipient name (_Some Name \<user@domain.tld\>_ or _withoutname@domain.tld_).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// You can create a `Mailbox` from a string and an [`Address`]:
|
||||
///
|
||||
/// ```
|
||||
/// # use lettre::{Address, message::Mailbox};
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mailbox = Mailbox::new(None, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can also create one from a string literal:
|
||||
///
|
||||
/// ```
|
||||
/// # use lettre::message::Mailbox;
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let mailbox: Mailbox = "John Smith <example@email.com>".parse()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailbox {
|
||||
/// The name associated with the address.
|
||||
pub name: Option<String>,
|
||||
|
||||
/// The email address itself.
|
||||
pub email: Address,
|
||||
}
|
||||
|
||||
impl Mailbox {
|
||||
/// Creates a new `Mailbox` using an email address and the name of the recipient if there is one.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{message::Mailbox, Address};
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mailbox = Mailbox::new(None, address);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(name: Option<String>, email: Address) -> Self {
|
||||
Mailbox { name, email }
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||
if let Some(name) = &self.name {
|
||||
email_encoding::headers::quoted_string::encode(name, w)?;
|
||||
w.space();
|
||||
w.write_char('<')?;
|
||||
}
|
||||
|
||||
w.write_str(self.email.as_ref())?;
|
||||
|
||||
if self.name.is_some() {
|
||||
w.write_char('>')?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
if let Some(name) = &self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
write_word(f, name)?;
|
||||
f.write_str(" <")?;
|
||||
self.email.fmt(f)?;
|
||||
return f.write_char('>');
|
||||
}
|
||||
}
|
||||
self.email.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> TryFrom<(S, T)> for Mailbox {
|
||||
type Error = AddressError;
|
||||
|
||||
fn try_from(header: (S, T)) -> Result<Self, Self::Error> {
|
||||
let (name, address) = header;
|
||||
Ok(Mailbox::new(Some(name.into()), address.into().parse()?))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
let (name, (user, domain)) = parsers::mailbox().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
let mailbox = Mailbox::new(name, Address::new(user, domain)?);
|
||||
|
||||
Ok(mailbox)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Address> for Mailbox {
|
||||
fn from(value: Address) -> Self {
|
||||
Self::new(None, value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a sequence of [`Mailbox`] instances.
|
||||
///
|
||||
/// This type contains a sequence of mailboxes (_Some Name \<user@domain.tld\>, Another Name \<other@domain.tld\>, withoutname@domain.tld, ..._).
|
||||
///
|
||||
/// **NOTE**: Enable feature "serde" to be able to serialize/deserialize it using [serde](https://serde.rs/).
|
||||
#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
|
||||
pub struct Mailboxes(Vec<Mailbox>);
|
||||
|
||||
impl Mailboxes {
|
||||
/// Creates a new list of [`Mailbox`] instances.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::message::Mailboxes;
|
||||
/// let mailboxes = Mailboxes::new();
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Mailboxes(Vec::new())
|
||||
}
|
||||
|
||||
/// Adds a new [`Mailbox`] to the list, in a builder style pattern.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mut mailboxes = Mailboxes::new().with(Mailbox::new(None, address));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn with(mut self, mbox: Mailbox) -> Self {
|
||||
self.0.push(mbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a new [`Mailbox`] to the list, in a `Vec::push` style pattern.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn push(&mut self, mbox: Mailbox) {
|
||||
self.0.push(mbox);
|
||||
}
|
||||
|
||||
/// Extracts the first [`Mailbox`] if it exists.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let empty = Mailboxes::new();
|
||||
/// assert!(empty.into_single().is_none());
|
||||
///
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
///
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
/// assert!(mailboxes.into_single().is_some());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn into_single(self) -> Option<Mailbox> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
/// Creates an iterator over the [`Mailbox`] instances that are currently stored.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::{
|
||||
/// message::{Mailbox, Mailboxes},
|
||||
/// Address,
|
||||
/// };
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let mut mailboxes = Mailboxes::new();
|
||||
///
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
///
|
||||
/// let address = Address::new("example", "email.com")?;
|
||||
/// mailboxes.push(Mailbox::new(None, address));
|
||||
///
|
||||
/// let mut iter = mailboxes.iter();
|
||||
///
|
||||
/// assert!(iter.next().is_some());
|
||||
/// assert!(iter.next().is_some());
|
||||
///
|
||||
/// assert!(iter.next().is_none());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn iter(&self) -> Iter<'_, Mailbox> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, w: &mut EmailWriter<'_>) -> FmtResult {
|
||||
let mut first = true;
|
||||
for mailbox in self.iter() {
|
||||
if !mem::take(&mut first) {
|
||||
w.write_char(',')?;
|
||||
w.space();
|
||||
}
|
||||
|
||||
mailbox.encode(w)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mailboxes {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mailbox> for Mailboxes {
|
||||
fn from(mailbox: Mailbox) -> Self {
|
||||
Mailboxes(vec![mailbox])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mailboxes> for Option<Mailbox> {
|
||||
fn from(mailboxes: Mailboxes) -> Option<Mailbox> {
|
||||
mailboxes.into_iter().next()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Mailbox>> for Mailboxes {
|
||||
fn from(vec: Vec<Mailbox>) -> Self {
|
||||
Mailboxes(vec)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mailboxes> for Vec<Mailbox> {
|
||||
fn from(mailboxes: Mailboxes) -> Vec<Mailbox> {
|
||||
mailboxes.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Mailbox> for Mailboxes {
|
||||
fn from_iter<T: IntoIterator<Item = Mailbox>>(iter: T) -> Self {
|
||||
Self(Vec::from_iter(iter))
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Mailbox> for Mailboxes {
|
||||
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
||||
self.0.extend(iter);
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for Mailboxes {
|
||||
type Item = Mailbox;
|
||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailboxes {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut iter = self.iter();
|
||||
|
||||
if let Some(mbox) = iter.next() {
|
||||
mbox.fmt(f)?;
|
||||
|
||||
for mbox in iter {
|
||||
f.write_str(", ")?;
|
||||
mbox.fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Mailboxes {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||
let mut mailboxes = Vec::new();
|
||||
|
||||
let parsed_mailboxes = parsers::mailbox_list().parse(src).map_err(|_errs| {
|
||||
// TODO: improve error management
|
||||
AddressError::InvalidInput
|
||||
})?;
|
||||
|
||||
for (name, (user, domain)) in parsed_mailboxes {
|
||||
mailboxes.push(Mailbox::new(name, Address::new(user, domain)?));
|
||||
}
|
||||
|
||||
Ok(Mailboxes(mailboxes))
|
||||
}
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.6
|
||||
fn write_word(f: &mut Formatter<'_>, s: &str) -> FmtResult {
|
||||
if s.as_bytes().iter().copied().all(is_valid_atom_char) {
|
||||
f.write_str(s)
|
||||
} else {
|
||||
// Quoted string: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||
f.write_char('"')?;
|
||||
for c in s.chars() {
|
||||
write_quoted_string_char(f, c)?;
|
||||
}
|
||||
f.write_char('"')?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.4
|
||||
fn is_valid_atom_char(c: u8) -> bool {
|
||||
matches!(c,
|
||||
// Not really allowed but can be inserted between atoms.
|
||||
b'\t' |
|
||||
b' ' |
|
||||
|
||||
b'!' |
|
||||
b'#' |
|
||||
b'$' |
|
||||
b'%' |
|
||||
b'&' |
|
||||
b'\'' |
|
||||
b'*' |
|
||||
b'+' |
|
||||
b'-' |
|
||||
b'/' |
|
||||
b'0'..=b'8' |
|
||||
b'=' |
|
||||
b'?' |
|
||||
b'A'..=b'Z' |
|
||||
b'^' |
|
||||
b'_' |
|
||||
b'`' |
|
||||
b'a'..=b'z' |
|
||||
b'{' |
|
||||
b'|' |
|
||||
b'}' |
|
||||
b'~' |
|
||||
|
||||
// Not technically allowed but will be escaped into allowed characters.
|
||||
128..=255)
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.5
|
||||
fn write_quoted_string_char(f: &mut Formatter<'_>, c: char) -> FmtResult {
|
||||
match c {
|
||||
// Can not be encoded.
|
||||
'\n' | '\r' => Err(std::fmt::Error),
|
||||
|
||||
// Note, not qcontent but can be put before or after any qcontent.
|
||||
'\t' | ' ' => f.write_char(c),
|
||||
|
||||
c if match c as u32 {
|
||||
// NO-WS-CTL: https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.1
|
||||
1..=8 | 11 | 12 | 14..=31 | 127 |
|
||||
|
||||
// The rest of the US-ASCII except \ and "
|
||||
33 |
|
||||
35..=91 |
|
||||
93..=126 |
|
||||
|
||||
// Non-ascii characters will be escaped separately later.
|
||||
128.. => true,
|
||||
_ => false,
|
||||
} =>
|
||||
{
|
||||
f.write_char(c)
|
||||
}
|
||||
|
||||
_ => {
|
||||
// quoted-pair https://datatracker.ietf.org/doc/html/rfc2822#section-3.2.2
|
||||
f.write_char('\\')?;
|
||||
f.write_char(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::Mailbox;
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_only() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(None, "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("K.".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"\"K.\" <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_comma() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some("Last, First".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Last, First" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_comma_and_non_ascii() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some("Laşt, First".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Laşt, First" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_comma_and_quoted_non_ascii() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some(r#"Laşt, "First""#.into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Laşt, \"First\"" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mailbox_format_address_with_color() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(
|
||||
Some("Chris's Wiki :: blog".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
)
|
||||
),
|
||||
r#""Chris's Wiki :: blog" <kayo@example.com>"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("".to_owned()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"kayo@example.com"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_address_with_name_trim() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some(" K. ".into()), "kayo@example.com".parse().unwrap())
|
||||
),
|
||||
"\"K.\" <kayo@example.com>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_only() {
|
||||
assert_eq!(
|
||||
"kayo@example.com".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_only_trim() {
|
||||
assert_eq!(
|
||||
" kayo@example.com ".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_name() {
|
||||
assert_eq!(
|
||||
"K. <kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_name_trim() {
|
||||
assert_eq!(
|
||||
" K. <kayo@example.com> ".parse(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
"<kayo@example.com>".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_with_empty_name_trim() {
|
||||
assert_eq!(
|
||||
" <kayo@example.com> ".parse(),
|
||||
Ok(Mailbox::new(None, "kayo@example.com".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_address_from_tuple() {
|
||||
assert_eq!(
|
||||
("K.".to_owned(), "kayo@example.com".to_owned()).try_into(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
728
src/message/mimebody.rs
Normal file
728
src/message/mimebody.rs
Normal file
@@ -0,0 +1,728 @@
|
||||
use std::{io::Write, iter::repeat_with};
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use crate::message::{
|
||||
header::{self, ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat, IntoBody,
|
||||
};
|
||||
|
||||
/// MIME part variants
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) enum Part {
|
||||
/// Single part with content
|
||||
Single(SinglePart),
|
||||
|
||||
/// Multiple parts of content
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
#[cfg(feature = "dkim")]
|
||||
pub(super) fn format_body(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Part::Single(part) => part.format_body(out),
|
||||
Part::Multi(part) => part.format_body(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Part {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
match self {
|
||||
Part::Single(part) => part.format(out),
|
||||
Part::Multi(part) => part.format(out),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates builder for single part
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePartBuilder {
|
||||
headers: Headers,
|
||||
}
|
||||
|
||||
impl SinglePartBuilder {
|
||||
/// Creates a default singlepart builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the header to singlepart
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Type header of the singlepart
|
||||
pub fn content_type(mut self, content_type: ContentType) -> Self {
|
||||
self.headers.set(content_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build singlepart using body
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> SinglePart {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
self.headers.set(body.encoding());
|
||||
|
||||
SinglePart {
|
||||
headers: self.headers,
|
||||
body: body.into_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SinglePartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Single part
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use lettre::message::{header, SinglePart};
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Текст письма в уникоде"));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SinglePart {
|
||||
headers: Headers,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
|
||||
impl SinglePart {
|
||||
/// Creates a builder for singlepart
|
||||
#[inline]
|
||||
pub fn builder() -> SinglePartBuilder {
|
||||
SinglePartBuilder::new()
|
||||
}
|
||||
|
||||
/// Directly create a `SinglePart` from a plain UTF-8 content
|
||||
pub fn plain<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Directly create a `SinglePart` from a UTF-8 HTML content
|
||||
pub fn html<T: IntoBody>(body: T) -> Self {
|
||||
Self::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(body)
|
||||
}
|
||||
|
||||
/// Get the headers from singlepart
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get the encoded body
|
||||
#[inline]
|
||||
pub fn raw_body(&self) -> &[u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Get message content formatted for sending
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Format only the signlepart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
/// The kind of multipart
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MultiPartKind {
|
||||
/// Mixed kind to combine unrelated content parts
|
||||
///
|
||||
/// For example, this kind can be used to mix an email message and attachments.
|
||||
Mixed,
|
||||
|
||||
/// Alternative kind to join several variants of same email contents.
|
||||
///
|
||||
/// That kind is recommended to use for joining plain (text) and rich (HTML) messages into a single email message.
|
||||
Alternative,
|
||||
|
||||
/// Related kind to mix content and related resources.
|
||||
///
|
||||
/// For example, you can include images in HTML content using that.
|
||||
Related,
|
||||
|
||||
/// Encrypted kind for encrypted messages
|
||||
Encrypted { protocol: String },
|
||||
|
||||
/// Signed kind for signed messages
|
||||
Signed { protocol: String, micalg: String },
|
||||
}
|
||||
|
||||
/// Create a random MIME boundary.
|
||||
/// (Not cryptographically random)
|
||||
fn make_boundary() -> String {
|
||||
repeat_with(fastrand::alphanumeric).take(40).collect()
|
||||
}
|
||||
|
||||
impl MultiPartKind {
|
||||
pub(crate) fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, Into::into);
|
||||
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"{}",
|
||||
match self {
|
||||
Self::Mixed => "mixed",
|
||||
Self::Alternative => "alternative",
|
||||
Self::Related => "related",
|
||||
Self::Encrypted { .. } => "encrypted",
|
||||
Self::Signed { .. } => "signed",
|
||||
},
|
||||
boundary,
|
||||
match self {
|
||||
Self::Encrypted { protocol } => format!("; protocol=\"{protocol}\""),
|
||||
Self::Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{protocol}\"; micalg=\"{micalg}\""),
|
||||
_ => String::new(),
|
||||
}
|
||||
)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Self::Mixed),
|
||||
"alternative" => Some(Self::Alternative),
|
||||
"related" => Some(Self::Related),
|
||||
"signed" => m.get_param("protocol").and_then(|p| {
|
||||
m.get_param("micalg").map(|micalg| Self::Signed {
|
||||
protocol: p.as_str().to_owned(),
|
||||
micalg: micalg.as_str().to_owned(),
|
||||
})
|
||||
}),
|
||||
"encrypted" => m.get_param("protocol").map(|p| Self::Encrypted {
|
||||
protocol: p.as_str().to_owned(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart builder
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPartBuilder {
|
||||
headers: Headers,
|
||||
}
|
||||
|
||||
impl MultiPartBuilder {
|
||||
/// Creates default multipart builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a header
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set `Content-Type` header using [`MultiPartKind`]
|
||||
pub fn kind(self, kind: MultiPartKind) -> Self {
|
||||
self.header(ContentType::from_mime(kind.to_mime::<String>(None)))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: Into<String>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
MultiPartKind::from_mime(content_type.as_ref()).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary));
|
||||
self.header(ContentType::from_mime(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
pub fn build(self) -> MultiPart {
|
||||
MultiPart {
|
||||
headers: self.headers,
|
||||
parts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates multipart using singlepart
|
||||
pub fn singlepart(self, part: SinglePart) -> MultiPart {
|
||||
self.build().singlepart(part)
|
||||
}
|
||||
|
||||
/// Creates multipart using multipart
|
||||
pub fn multipart(self, part: MultiPart) -> MultiPart {
|
||||
self.build().multipart(part)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MultiPartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Multipart variant with parts
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MultiPart {
|
||||
headers: Headers,
|
||||
parts: Vec<Part>,
|
||||
}
|
||||
|
||||
impl MultiPart {
|
||||
/// Creates multipart builder
|
||||
pub fn builder() -> MultiPartBuilder {
|
||||
MultiPartBuilder::new()
|
||||
}
|
||||
|
||||
/// Creates mixed multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Mixed)`
|
||||
pub fn mixed() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Mixed)
|
||||
}
|
||||
|
||||
/// Creates alternative multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Alternative)`
|
||||
pub fn alternative() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Alternative)
|
||||
}
|
||||
|
||||
/// Creates related multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Related)`
|
||||
pub fn related() -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Related)
|
||||
}
|
||||
|
||||
/// Creates encrypted multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Encrypted{ protocol })`
|
||||
pub fn encrypted(protocol: String) -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Encrypted { protocol })
|
||||
}
|
||||
|
||||
/// Creates signed multipart builder
|
||||
///
|
||||
/// Shortcut for `MultiPart::builder().kind(MultiPartKind::Signed{ protocol, micalg })`
|
||||
pub fn signed(protocol: String, micalg: String) -> MultiPartBuilder {
|
||||
MultiPart::builder().kind(MultiPartKind::Signed { protocol, micalg })
|
||||
}
|
||||
|
||||
/// Alias for HTML and plain text versions of an email
|
||||
pub fn alternative_plain_html<T: IntoBody, V: IntoBody>(plain: T, html: V) -> Self {
|
||||
Self::alternative()
|
||||
.singlepart(SinglePart::plain(plain))
|
||||
.singlepart(SinglePart::html(html))
|
||||
}
|
||||
|
||||
/// Add single part to multipart
|
||||
pub fn singlepart(mut self, part: SinglePart) -> Self {
|
||||
self.parts.push(Part::Single(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multi part to multipart
|
||||
pub fn multipart(mut self, part: MultiPart) -> Self {
|
||||
self.parts.push(Part::Multi(part));
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the boundary of multipart contents
|
||||
pub fn boundary(&self) -> String {
|
||||
let content_type = self.headers.get::<ContentType>().unwrap();
|
||||
content_type
|
||||
.as_ref()
|
||||
.get_param("boundary")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Get the headers from the multipart
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
/// Format only the multipart body
|
||||
fn format_body(&self, out: &mut Vec<u8>) {
|
||||
let boundary = self.boundary();
|
||||
|
||||
for part in &self.parts {
|
||||
out.extend_from_slice(b"--");
|
||||
out.extend_from_slice(boundary.as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
part.format(out);
|
||||
}
|
||||
|
||||
out.extend_from_slice(b"--");
|
||||
out.extend_from_slice(boundary.as_bytes());
|
||||
out.extend_from_slice(b"--\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
out.extend_from_slice(b"\r\n");
|
||||
self.format_body(out);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_quoted_printable() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: quoted-printable\r\n",
|
||||
"\r\n",
|
||||
"=D0=A2=D0=B5=D0=BA=D1=81=D1=82 =D0=BF=D0=B8=D1=81=D1=8C=D0=BC=D0=B0 =D0=B2 =\r\n",
|
||||
"=D1=83=D0=BD=D0=B8=D0=BA=D0=BE=D0=B4=D0=B5\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_part_base64() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/mixed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_encrypted() {
|
||||
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/octet-stream; name=\"encrypted.asc\"")
|
||||
.unwrap(),
|
||||
)
|
||||
.header(header::ContentDisposition::inline_with_name(
|
||||
"encrypted.asc",
|
||||
))
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n"
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/encrypted;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-encrypted\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Version: 1\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
|
||||
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP MESSAGE-----\r\n",
|
||||
"wV4D0dz5vDXklO8SAQdA5lGX1UU/eVQqDxNYdHa7tukoingHzqUB6wQssbMfHl8w\r\n",
|
||||
"...\r\n",
|
||||
"-----END PGP MESSAGE-----\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_signed() {
|
||||
let part = MultiPart::signed(
|
||||
"application/pgp-signature".to_owned(),
|
||||
"pgp-sha256".to_owned(),
|
||||
)
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.body(String::from("Test email for signature")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(
|
||||
ContentType::parse("application/pgp-signature; name=\"signature.asc\"")
|
||||
.unwrap(),
|
||||
)
|
||||
.header(header::ContentDisposition::attachment("signature.asc"))
|
||||
.body(String::from(concat!(
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
|
||||
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
|
||||
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
|
||||
"=3FYZ\r\n",
|
||||
"-----END PGP SIGNATURE-----\r\n",
|
||||
))),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: multipart/signed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\";\r\n",
|
||||
" protocol=\"application/pgp-signature\";",
|
||||
" micalg=\"pgp-sha256\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Test email for signature\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: application/pgp-signature; name=\"signature.asc\"\r\n",
|
||||
"Content-Disposition: attachment; filename=\"signature.asc\"\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"-----BEGIN PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"iHUEARYIAB0WIQTNsp3S/GbdE0KoiQ+IGQOscREZuQUCXyOzDAAKCRCIGQOscREZ\r\n",
|
||||
"udgDAQCv3FJ3QWW5bRaGZAa0Ug6vASFdkvDMKoRwcoFnHPthjQEAiQ8skkIyE2GE\r\n",
|
||||
"PoLpAXiKpT+NU8S8+8dfvwutnb4dSwM=\r\n",
|
||||
"=3FYZ\r\n",
|
||||
"-----END PGP SIGNATURE-----\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_alternative() {
|
||||
let part = MultiPart::alternative()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/alternative;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>")))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentLocation::from(String::from("/image.png")))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_PLAIN)
|
||||
.header(header::ContentDisposition::attachment("example.c"))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: multipart/related;\r\n",
|
||||
" boundary=\"0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\"\r\n",
|
||||
"\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/html; charset=utf-8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: image/png\r\n",
|
||||
"Content-Location: /image.png\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3\r\n",
|
||||
"ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0\r\n",
|
||||
"NTY3ODkwMTIzNDU2Nzg5MA==\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1\r\n",
|
||||
"Content-Type: text/plain; charset=utf-8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_boundary() {
|
||||
let mut boundaries = std::collections::HashSet::with_capacity(10);
|
||||
for _ in 0..1000 {
|
||||
boundaries.insert(make_boundary());
|
||||
}
|
||||
|
||||
// Ensure there are no duplicates
|
||||
assert_eq!(1000, boundaries.len());
|
||||
|
||||
// Ensure correct length
|
||||
for boundary in boundaries {
|
||||
assert_eq!(40, boundary.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
791
src/message/mod.rs
Normal file
791
src/message/mod.rs
Normal file
@@ -0,0 +1,791 @@
|
||||
//! Provides a strongly typed way to build emails
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! This section demonstrates how to build messages.
|
||||
//!
|
||||
//! <style>
|
||||
//! summary, details:not([open]) { cursor: pointer; }
|
||||
//! </style>
|
||||
//!
|
||||
//!
|
||||
//! ### Plain body
|
||||
//!
|
||||
//! The easiest way of creating a message, which uses a plain text body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::{header::ContentType, Message};
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||
//! Content-Type: text/plain; charset=utf-8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//! </details>
|
||||
//! <br />
|
||||
//!
|
||||
//! The unicode header data is encoded using _UTF8-Base64_ encoding, when necessary.
|
||||
//!
|
||||
//! The `Content-Transfer-Encoding` is chosen based on the best encoding
|
||||
//! available for the given body, between `7bit`, `quoted-printable` and `base64`.
|
||||
//!
|
||||
//! ### Plain and HTML body
|
||||
//!
|
||||
//! Uses a MIME body to include both plain text and HTML versions of the body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! use lettre::message::{header, Message, MultiPart, SinglePart};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .multipart(MultiPart::alternative_plain_html(
|
||||
//! String::from("Hello, world! :)"),
|
||||
//! String::from("<p><b>Hello</b>, <i>world</i>! <img src=\"cid:123\"></p>"),
|
||||
//! ))?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! MIME-Version: 1.0
|
||||
//! Date: Sat, 12 Dec 2020 16:33:19 GMT
|
||||
//! Content-Type: multipart/alternative; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
//!
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! Hello, world! :)
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/html; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src="cid:123"></p>
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
//! ```
|
||||
//! </details>
|
||||
//!
|
||||
//! ### Complex MIME body
|
||||
//!
|
||||
//! This example shows how to include both plain and HTML versions of the body,
|
||||
//! attachments and inlined images.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! use lettre::message::{header, Attachment, Body, Message, MultiPart, SinglePart};
|
||||
//!
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let image = fs::read("docs/lettre.png")?;
|
||||
//! // this image_body can be cloned and reused between emails.
|
||||
//! // since `Body` holds a pre-encoded body, reusing it means avoiding having
|
||||
//! // to re-encode the same body for every email (this clearly only applies
|
||||
//! // when sending multiple emails with the same attachment).
|
||||
//! let image_body = Body::new(image);
|
||||
//!
|
||||
//! let m = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .multipart(
|
||||
//! MultiPart::mixed()
|
||||
//! .multipart(
|
||||
//! MultiPart::alternative()
|
||||
//! .singlepart(SinglePart::plain(String::from("Hello, world! :)")))
|
||||
//! .multipart(
|
||||
//! MultiPart::related()
|
||||
//! .singlepart(SinglePart::html(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
//! )))
|
||||
//! .singlepart(
|
||||
//! Attachment::new_inline(String::from("123"))
|
||||
//! .body(image_body, "image/png".parse().unwrap()),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! .singlepart(Attachment::new(String::from("example.rs")).body(
|
||||
//! String::from("fn main() { println!(\"Hello, World!\") }"),
|
||||
//! "text/plain".parse().unwrap(),
|
||||
//! )),
|
||||
//! )?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Which produces:
|
||||
//! <details>
|
||||
//! <summary>Click to expand</summary>
|
||||
//!
|
||||
//! ```sh
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! MIME-Version: 1.0
|
||||
//! Date: Sat, 12 Dec 2020 16:30:45 GMT
|
||||
//! Content-Type: multipart/mixed; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
//!
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: multipart/alternative; boundary="EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk"
|
||||
//!
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! Hello, world! :)
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk
|
||||
//! Content-Type: multipart/related; boundary="eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr"
|
||||
//!
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
|
||||
//! Content-Type: text/html; charset=utf8
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! <p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr
|
||||
//! Content-Type: image/png
|
||||
//! Content-Disposition: inline
|
||||
//! Content-ID: <123>
|
||||
//! Content-Transfer-Encoding: base64
|
||||
//!
|
||||
//! PHNtaWxlLXJhdy1pbWFnZS1kYXRhPg==
|
||||
//! --eM5Z18WZVOQsqi5GQ71XGAXk6NNvHUA1Xv1FWrXr--
|
||||
//! --EyXdAZIgZuyUjAounq4Aj44a6MpJfqCKhm6pE1zk--
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
//! Content-Type: text/plain; charset=utf8
|
||||
//! Content-Disposition: attachment; filename="example.rs"
|
||||
//! Content-Transfer-Encoding: 7bit
|
||||
//!
|
||||
//! fn main() { println!("Hello, World!") }
|
||||
//! --0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
//! ```
|
||||
//! </details>
|
||||
|
||||
use std::{io::Write, iter, time::SystemTime};
|
||||
|
||||
pub use attachment::Attachment;
|
||||
pub use body::{Body, IntoBody, MaybeString};
|
||||
#[cfg(feature = "dkim")]
|
||||
pub use dkim::*;
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
mod attachment;
|
||||
mod body;
|
||||
#[cfg(feature = "dkim")]
|
||||
pub mod dkim;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, Header, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
|
||||
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
|
||||
|
||||
/// Something that can be formatted as an email message
|
||||
trait EmailFormat {
|
||||
// Use a writer?
|
||||
fn format(&self, out: &mut Vec<u8>);
|
||||
}
|
||||
|
||||
/// A builder for messages
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MessageBuilder {
|
||||
headers: Headers,
|
||||
envelope: Option<Envelope>,
|
||||
drop_bcc: bool,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
/// Creates a new default message builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
envelope: None,
|
||||
drop_bcc: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `From` header
|
||||
///
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::From(mbox))`.
|
||||
pub fn from(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::From::from(Mailboxes::from(mbox)))
|
||||
}
|
||||
|
||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||
///
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.header(header::Sender(mbox))`.
|
||||
pub fn sender(self, mbox: Mailbox) -> Self {
|
||||
self.header(header::Sender::from(mbox))
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date::new(st))`.
|
||||
pub fn date(self, st: SystemTime) -> Self {
|
||||
self.header(header::Date::new(st))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`, it is automatically inserted
|
||||
/// if no date has been provided.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(crate::time::now())
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
///
|
||||
/// Defined in [RFC5322](https://tools.ietf.org/html/rfc5322#section-3.6.2).
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::ReplyTo(mbox))`.
|
||||
pub fn reply_to(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::ReplyTo(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `To` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::To(mbox))`.
|
||||
pub fn to(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::To(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Cc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Cc(mbox))`.
|
||||
pub fn cc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Cc(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `Bcc` header
|
||||
///
|
||||
/// Shortcut for `self.mailbox(header::Bcc(mbox))`.
|
||||
pub fn bcc(self, mbox: Mailbox) -> Self {
|
||||
self.mailbox(header::Bcc(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`In-Reply-To`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn in_reply_to(self, id: String) -> Self {
|
||||
self.header(header::InReplyTo::from(id))
|
||||
}
|
||||
|
||||
/// Set or add message id to [`References`
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
pub fn references(self, id: String) -> Self {
|
||||
self.header(header::References::from(id))
|
||||
}
|
||||
|
||||
/// Set `Subject` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||
let s: String = subject.into();
|
||||
self.header(header::Subject::from(s))
|
||||
}
|
||||
|
||||
/// Set [Message-ID
|
||||
/// header](https://tools.ietf.org/html/rfc5322#section-3.6.4)
|
||||
///
|
||||
/// Should generally be inserted by the mail relay.
|
||||
///
|
||||
/// If `None` is provided, an id will be generated in the
|
||||
/// `<UUID@HOSTNAME>`.
|
||||
pub fn message_id(self, id: Option<String>) -> Self {
|
||||
match id {
|
||||
Some(i) => self.header(header::MessageId::from(i)),
|
||||
None => {
|
||||
#[cfg(feature = "hostname")]
|
||||
let hostname = hostname::get()
|
||||
.map_err(|_| ())
|
||||
.and_then(|s| s.into_string().map_err(|_| ()))
|
||||
.unwrap_or_else(|()| DEFAULT_MESSAGE_ID_DOMAIN.to_owned());
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_owned();
|
||||
|
||||
self.header(header::MessageId::from(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", make_message_id(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set [User-Agent
|
||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-00)
|
||||
pub fn user_agent(self, id: String) -> Self {
|
||||
self.header(header::UserAgent::from(id))
|
||||
}
|
||||
|
||||
/// Set custom header to message
|
||||
pub fn header<H: Header>(mut self, header: H) -> Self {
|
||||
self.headers.set(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add mailbox to header
|
||||
pub fn mailbox<H: Header + MailboxesHeader>(self, header: H) -> Self {
|
||||
match self.headers.get::<H>() {
|
||||
Some(mut header_) => {
|
||||
header_.join_mailboxes(header);
|
||||
self.header(header_)
|
||||
}
|
||||
None => self.header(header),
|
||||
}
|
||||
}
|
||||
|
||||
/// Force specific envelope (by default it is derived from headers)
|
||||
pub fn envelope(mut self, envelope: Envelope) -> Self {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
/// Keep the `Bcc` header
|
||||
///
|
||||
/// By default, the `Bcc` header is removed from the email after
|
||||
/// using it to generate the message envelope. In some cases though,
|
||||
/// like when saving the email as an `.eml`, or sending through
|
||||
/// some transports (like the Gmail API) that don't take a separate
|
||||
/// envelope value, it becomes necessary to keep the `Bcc` header.
|
||||
///
|
||||
/// Calling this method overrides the default behavior.
|
||||
pub fn keep_bcc(mut self) -> Self {
|
||||
self.drop_bcc = false;
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: High-level methods for attachments and embedded files
|
||||
|
||||
/// Create message from body
|
||||
fn build(self, body: MessageBody) -> Result<Message, EmailError> {
|
||||
// Check for missing required headers
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6
|
||||
|
||||
// Insert Date if missing
|
||||
let mut res = if self.headers.get::<header::Date>().is_none() {
|
||||
self.date_now()
|
||||
} else {
|
||||
self
|
||||
};
|
||||
|
||||
// Fail is missing correct originator (Sender or From)
|
||||
match res.headers.get::<header::From>() {
|
||||
Some(header::From(f)) => {
|
||||
let from: Vec<Mailbox> = f.into();
|
||||
if from.len() > 1 && res.headers.get::<header::Sender>().is_none() {
|
||||
return Err(EmailError::TooManyFrom);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(EmailError::MissingFrom);
|
||||
}
|
||||
}
|
||||
|
||||
let envelope = match res.envelope {
|
||||
Some(e) => e,
|
||||
None => Envelope::try_from(&res.headers)?,
|
||||
};
|
||||
|
||||
if res.drop_bcc {
|
||||
// Remove `Bcc` headers now the envelope is set
|
||||
res.headers.remove::<header::Bcc>();
|
||||
}
|
||||
|
||||
Ok(Message {
|
||||
headers: res.headers,
|
||||
body,
|
||||
envelope,
|
||||
})
|
||||
}
|
||||
|
||||
/// Create [`Message`] using a [`Vec<u8>`], [`String`], or [`Body`] body
|
||||
///
|
||||
/// Automatically gets encoded with `7bit`, `quoted-printable` or `base64`
|
||||
/// `Content-Transfer-Encoding`, based on the most efficient and valid encoding
|
||||
/// for `body`.
|
||||
pub fn body<T: IntoBody>(mut self, body: T) -> Result<Message, EmailError> {
|
||||
let maybe_encoding = self.headers.get::<ContentTransferEncoding>();
|
||||
let body = body.into_body(maybe_encoding);
|
||||
|
||||
self.headers.set(body.encoding());
|
||||
self.build(MessageBody::Raw(body.into_vec()))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`MultiPart`])
|
||||
pub fn multipart(self, part: MultiPart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Multi(part)))
|
||||
}
|
||||
|
||||
/// Create message using mime body ([`SinglePart`])
|
||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
||||
}
|
||||
|
||||
/// Set `MIME-Version` header to 1.0
|
||||
///
|
||||
/// Shortcut for `self.header(header::MIME_VERSION_1_0)`.
|
||||
///
|
||||
/// Not exposed as it is set by body methods
|
||||
fn mime_1_0(self) -> Self {
|
||||
self.header(header::MIME_VERSION_1_0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message which can be formatted
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Message {
|
||||
headers: Headers,
|
||||
body: MessageBody,
|
||||
envelope: Envelope,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum MessageBody {
|
||||
Mime(Part),
|
||||
Raw(Vec<u8>),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Create a new message builder without headers
|
||||
pub fn builder() -> MessageBuilder {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
|
||||
/// Get the headers from the Message
|
||||
pub fn headers(&self) -> &Headers {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
pub fn headers_mut(&mut self) -> &mut Headers {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Get `Message` envelope
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(feature = "dkim")]
|
||||
/// Format body for signing
|
||||
pub(crate) fn body_raw(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format_body(&mut out),
|
||||
MessageBody::Raw(r) => out.extend_from_slice(r),
|
||||
}
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out
|
||||
}
|
||||
|
||||
/// Sign the message using Dkim
|
||||
///
|
||||
/// Example:
|
||||
/// ```rust
|
||||
/// use lettre::{
|
||||
/// message::{
|
||||
/// dkim::{DkimConfig, DkimSigningAlgorithm, DkimSigningKey},
|
||||
/// header::ContentType,
|
||||
/// },
|
||||
/// Message,
|
||||
/// };
|
||||
///
|
||||
/// let mut message = Message::builder()
|
||||
/// .from("Alice <alice@example.org>".parse().unwrap())
|
||||
/// .reply_to("Bob <bob@example.org>".parse().unwrap())
|
||||
/// .to("Carla <carla@example.net>".parse().unwrap())
|
||||
/// .subject("Hello")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body("Hi there, it's a test email, with utf-8 chars ë!\n\n\n".to_owned())
|
||||
/// .unwrap();
|
||||
/// let key = "-----BEGIN RSA PRIVATE KEY-----
|
||||
/// MIIEowIBAAKCAQEAt2gawjoybf0mAz0mSX0cq1ah5F9cPazZdCwLnFBhRufxaZB8
|
||||
/// NLTdc9xfPIOK8l/xGrN7Nd63J4cTATqZukumczkA46O8YKHwa53pNT6NYwCNtDUL
|
||||
/// eBu+7xUW18GmDzkIFkxGO2R5kkTeWPlKvKpEiicIMfl0OmyW/fI3AbtM7e/gmqQ4
|
||||
/// kEYIO0mTjPT+jTgWE4JIi5KUTHudUBtfMKcSFyM2HkUOExl1c9+A4epjRFQwEXMA
|
||||
/// hM5GrqZoOdUm4fIpvGpLIGIxFgHPpZYbyq6yJZzH3+5aKyCHrsHawPuPiCD45zsU
|
||||
/// re31zCE6b6k1sDiiBR4CaRHnbL7hxFp0aNLOVQIDAQABAoIBAGMK3gBrKxaIcUGo
|
||||
/// gQeIf7XrJ6vK72YC9L8uleqI4a9Hy++E7f4MedZ6eBeWta8jrnEL4Yp6xg+beuDc
|
||||
/// A24+Mhng+6Dyp+TLLqj+8pQlPnbrMprRVms7GIXFrrs+wO1RkBNyhy7FmH0roaMM
|
||||
/// pJZzoGW2pE9QdbqjL3rdlWTi/60xRX9eZ42nNxYnbc+RK03SBd46c3UBha6Y9iQX
|
||||
/// 562yWilDnB5WCX2tBoSN39bEhJvuZDzMwOuGw68Q96Hdz82Iz1xVBnRhH+uNStjR
|
||||
/// VnAssSHVxPSpwWrm3sHlhjBHWPnNIaOKIKl1lbL+qWfVQCj/6a5DquC+vYAeYR6L
|
||||
/// 3mA0z0ECgYEA5YkNYcILSXyE0hZ8eA/t58h8eWvYI5iqt3nT4fznCoYJJ74Vukeg
|
||||
/// 6BTlq/CsanwT1lDtvDKrOaJbA7DPTES/bqT0HoeIdOvAw9w/AZI5DAqYp61i6RMK
|
||||
/// xfAQL/Ik5MDFN8gEMLLXRVMe/aR27f6JFZpShJOK/KCzHqikKfYVJ+UCgYEAzI2F
|
||||
/// ZlTyittWSyUSl5UKyfSnFOx2+6vNy+lu5DeMJu8Wh9rqBk388Bxq98CfkCseWESN
|
||||
/// pTCGdYltz9DvVNBdBLwSMdLuYJAI6U+Zd70MWyuNdHFPyWVHUNqMUBvbUtj2w74q
|
||||
/// Hzu0GI0OrRjdX6C63S17PggmT/N2R9X7P4STxbECgYA+AZAD4I98Ao8+0aQ+Ks9x
|
||||
/// 1c8KXf+9XfiAKAD9A3zGcv72JXtpHwBwsXR5xkJNYcdaFfKi7G0k3J8JmDHnwIqW
|
||||
/// MSlhNeu+6hDg2BaNLhsLDbG/Wi9mFybJ4df9m8Qrp4efUgEPxsAwkgvFKTCXijMu
|
||||
/// CspP1iutoxvAJH50d22voQKBgDIsSFtIXNGYaTs3Va8enK3at5zXP3wNsQXiNRP/
|
||||
/// V/44yNL77EktmewfXFF2yuym1uOZtRCerWxpEClYO0wXa6l8pA3aiiPfUIBByQfo
|
||||
/// s/4s2Z6FKKfikrKPWLlRi+NvWl+65kQQ9eTLvJzSq4IIP61+uWsGvrb/pbSLFPyI
|
||||
/// fWKRAoGBALFCStBXvdMptjq4APUzAdJ0vytZzXkOZHxgmc+R0fQn22OiW0huW6iX
|
||||
/// JcaBbL6ZSBIMA3AdaIjtvNRiomueHqh0GspTgOeCE2585TSFnw6vEOJ8RlR4A0Mw
|
||||
/// I45fbR4l+3D/30WMfZlM6bzZbwPXEnr2s1mirmuQpjumY9wLhK25
|
||||
/// -----END RSA PRIVATE KEY-----";
|
||||
/// let signing_key = DkimSigningKey::new(key, DkimSigningAlgorithm::Rsa).unwrap();
|
||||
/// message.sign(&DkimConfig::default_config(
|
||||
/// "dkimtest".to_owned(),
|
||||
/// "example.org".to_owned(),
|
||||
/// signing_key,
|
||||
/// ));
|
||||
/// println!(
|
||||
/// "message: {}",
|
||||
/// std::str::from_utf8(&message.formatted()).unwrap()
|
||||
/// );
|
||||
/// ```
|
||||
#[cfg(feature = "dkim")]
|
||||
pub fn sign(&mut self, dkim_config: &DkimConfig) {
|
||||
dkim_sign(self, dkim_config);
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Message {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
write!(out, "{}", self.headers)
|
||||
.expect("A Write implementation panicked while formatting headers");
|
||||
|
||||
match &self.body {
|
||||
MessageBody::Mime(p) => p.format(out),
|
||||
MessageBody::Raw(r) => {
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MessageBuilder {
|
||||
fn default() -> Self {
|
||||
MessageBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a random message id.
|
||||
/// (Not cryptographically random)
|
||||
fn make_message_id() -> String {
|
||||
iter::repeat_with(fastrand::alphanumeric).take(36).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::{header, mailbox::Mailbox, make_message_id, Message, MultiPart, SinglePart};
|
||||
|
||||
#[test]
|
||||
fn email_missing_originator() {
|
||||
assert!(Message::builder()
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_minimal_message() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.to("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_missing_sender() {
|
||||
assert!(Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.from("AnyBody <anybody@domain.tld>".parse().unwrap())
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_message_no_bcc() {
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.bcc("hidden@example.com".parse().unwrap())
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
)]
|
||||
.into(),
|
||||
))
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject::from(String::from("яңа ел белән!")))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Happy new year!"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_message_keep_bcc() {
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.bcc("hidden@example.com".parse().unwrap())
|
||||
.keep_bcc()
|
||||
.header(header::From(
|
||||
vec![Mailbox::new(
|
||||
Some("Каи".into()),
|
||||
"kayo@example.com".parse().unwrap(),
|
||||
)]
|
||||
.into(),
|
||||
))
|
||||
.header(header::To(
|
||||
vec!["Pony O.P. <pony@domain.tld>".parse().unwrap()].into(),
|
||||
))
|
||||
.header(header::Subject::from(String::from("яңа ел белән!")))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 +0000\r\n",
|
||||
"Bcc: hidden@example.com\r\n",
|
||||
"From: =?utf-8?b?0JrQsNC4?= <kayo@example.com>\r\n",
|
||||
"To: \"Pony O.P.\" <pony@domain.tld>\r\n",
|
||||
"Subject: =?utf-8?b?0Y/So9CwINC10Lsg0LHQtdC705nQvSE=?=\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Happy new year!"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_with_png() {
|
||||
// Tue, 15 Nov 1994 08:12:31 GMT
|
||||
let date = SystemTime::UNIX_EPOCH + Duration::from_secs(784887151);
|
||||
let img = std::fs::read("./docs/lettre.png").unwrap();
|
||||
let m = Message::builder()
|
||||
.date(date)
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.multipart(
|
||||
MultiPart::related()
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::TEXT_HTML)
|
||||
.body(String::from(
|
||||
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
)),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType::parse("image/png").unwrap())
|
||||
.header(header::ContentDisposition::inline())
|
||||
.header(header::ContentId::from(String::from("<123>")))
|
||||
.body(img),
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let output = String::from_utf8(m.formatted()).unwrap();
|
||||
let file_expected = std::fs::read("./testdata/email_with_png.eml").unwrap();
|
||||
let expected = String::from_utf8(file_expected).unwrap();
|
||||
|
||||
for (i, line) in output.lines().zip(expected.lines()).enumerate() {
|
||||
if i == 7 || i == 9 || i == 14 || i == 233 {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_make_message_id() {
|
||||
let mut ids = std::collections::HashSet::with_capacity(10);
|
||||
for _ in 0..1000 {
|
||||
ids.insert(make_message_id());
|
||||
}
|
||||
|
||||
// Ensure there are no duplicates
|
||||
assert_eq!(1000, ids.len());
|
||||
|
||||
// Ensure correct length
|
||||
for id in ids {
|
||||
assert_eq!(36, id.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/rustls_crypto.rs
Normal file
14
src/rustls_crypto.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use rustls::crypto::CryptoProvider;
|
||||
|
||||
pub(crate) fn crypto_provider() -> Arc<CryptoProvider> {
|
||||
CryptoProvider::get_default().cloned().unwrap_or_else(|| {
|
||||
#[cfg(feature = "aws-lc-rs")]
|
||||
let provider = rustls::crypto::aws_lc_rs::default_provider();
|
||||
#[cfg(not(feature = "aws-lc-rs"))]
|
||||
let provider = rustls::crypto::ring::default_provider();
|
||||
|
||||
Arc::new(provider)
|
||||
})
|
||||
}
|
||||
26
src/time.rs
Normal file
26
src/time.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
#[cfg(all(feature = "web", target_arch = "wasm32"))]
|
||||
pub(crate) fn now() -> SystemTime {
|
||||
fn to_std_systemtime(time: web_time::SystemTime) -> std::time::SystemTime {
|
||||
let duration = time
|
||||
.duration_since(web_time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
SystemTime::UNIX_EPOCH + duration
|
||||
}
|
||||
|
||||
// FIXME: change to:
|
||||
// #[allow(
|
||||
// clippy::disallowed_methods,
|
||||
// reason = "`web-time` aliases `std::time::SystemTime::now` on non-WASM platforms"
|
||||
// )]
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
to_std_systemtime(web_time::SystemTime::now())
|
||||
}
|
||||
|
||||
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
|
||||
pub(crate) fn now() -> SystemTime {
|
||||
// FIXME: change to #[expect(clippy::disallowed_methods, reason = "the `web` feature is disabled")]
|
||||
#[allow(clippy::disallowed_methods)]
|
||||
SystemTime::now()
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::io;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use transport::smtp::response::{Response, Severity};
|
||||
use rustc_serialize::base64::FromBase64Error;
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
TransientError(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
PermanentError(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsingError(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsingError(FromBase64Error),
|
||||
/// Internal client error
|
||||
ClientError(&'static str),
|
||||
/// DNS resolution error
|
||||
ResolutionError,
|
||||
/// IO error
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
TransientError(_) => "a transient error occured during the SMTP transaction",
|
||||
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
||||
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
|
||||
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
|
||||
ResolutionError => "Could no resolve hostname",
|
||||
ClientError(_) => "an unknown error occured",
|
||||
IoError(_) => "an I/O error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
IoError(ref err) => Some(&*err as &StdError),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.severity() {
|
||||
Severity::TransientNegativeCompletion => TransientError(response),
|
||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||
_ => ClientError("Unknown error code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
ClientError(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type EmailResult = Result<Response, Error>;
|
||||
98
src/transport/file/error.rs
Normal file
98
src/transport/file/error.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
use crate::BoxError;
|
||||
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is a file I/O error
|
||||
pub fn is_io(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Io)
|
||||
}
|
||||
|
||||
/// Returns true if the error is an envelope serialization or deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn is_envelope(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// File I/O error
|
||||
Io,
|
||||
/// Envelope serialization/deserialization error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Envelope,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::file::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Io => f.write_str("response error")?,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Kind::Envelope => f.write_str("internal client error")?,
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn io<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Io, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub(crate) fn envelope<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Envelope, Some(e))
|
||||
}
|
||||
@@ -1,53 +1,348 @@
|
||||
//! This transport creates a file for each email, containing the enveloppe information and the email
|
||||
//! itself.
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.eml`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Sync example with envelope
|
||||
//!
|
||||
//! It is possible to also write the envelope content in a separate JSON file
|
||||
//! by using the `with_envelope` builder. The JSON file will be written in the
|
||||
//! target directory with same name and a `json` extension.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{message::header::ContentType, FileTransport, Message, Transport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport-envelope", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<Tokio1Executor>::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//!
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncFileTransport, AsyncStd1Executor, AsyncTransport,
|
||||
//! Message,
|
||||
//! };
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = AsyncFileTransport::<AsyncStd1Executor>::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Example email content result
|
||||
//!
|
||||
//! ```eml
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Content-Type: text/plain; charset=utf-8
|
||||
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//!
|
||||
//! Example envelope result
|
||||
//!
|
||||
//! ```json
|
||||
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
|
||||
//! ```
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::prelude::*;
|
||||
use std::fs::File;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
|
||||
use transport::EmailTransport;
|
||||
use transport::error::EmailResult;
|
||||
use transport::smtp::response::Response;
|
||||
use transport::smtp::response::{Category, Code, Severity};
|
||||
use email::SendableEmail;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Writes the content and the enveloppe information to a file
|
||||
pub struct FileEmailTransport {
|
||||
pub use self::error::Error;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
|
||||
mod error;
|
||||
|
||||
type Id = String;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: bool,
|
||||
}
|
||||
|
||||
impl FileEmailTransport {
|
||||
/// Asynchronously writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
pub struct AsyncFileTransport<E: Executor> {
|
||||
inner: FileTransport,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileEmailTransport {
|
||||
let mut path_buf = PathBuf::new();
|
||||
path_buf.push(path);
|
||||
FileEmailTransport { path: path_buf }
|
||||
///
|
||||
/// Writes the email content in eml format.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a message that was written using the file transport.
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
use std::fs;
|
||||
|
||||
let eml_file = self.path.join(format!("{email_id}.eml"));
|
||||
let eml = fs::read(eml_file).map_err(error::io)?;
|
||||
|
||||
let json_file = self.path.join(format!("{email_id}.json"));
|
||||
let json = fs::read(json_file).map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
|
||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||
self.path.join(format!("{email_id}.{extension}"))
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailTransport for FileEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.txt", email.message_id()));
|
||||
|
||||
let mut f = try!(File::create(file.as_path()));
|
||||
|
||||
let log_line = format!("{}: from=<{}> to=<{}>\n",
|
||||
email.message_id(),
|
||||
email.from_address(),
|
||||
email.to_addresses().join("> to=<"));
|
||||
|
||||
try!(f.write_all(log_line.as_bytes()));
|
||||
try!(f.write_all(format!("{}", email.message()).as_bytes()));
|
||||
|
||||
info!("{} status=<written>", log_line);
|
||||
|
||||
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
|
||||
vec![format!("Ok: email written to {}",
|
||||
file.to_str().unwrap_or("non-UTF-8 path"))]))
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
inner: FileTransport::new(path),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self) {
|
||||
()
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> Self {
|
||||
Self {
|
||||
inner: FileTransport::with_envelope(path),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a message that was written using the file transport.
|
||||
///
|
||||
/// Reads the envelope and the raw message content.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport-envelope")))]
|
||||
pub async fn read(&self, email_id: &str) -> Result<(Envelope, Vec<u8>), Error> {
|
||||
let eml_file = self.inner.path.join(format!("{email_id}.eml"));
|
||||
let eml = E::fs_read(&eml_file).await.map_err(error::io)?;
|
||||
|
||||
let json_file = self.inner.path.join(format!("{email_id}.json"));
|
||||
let json = E::fs_read(&json_file).await.map_err(error::io)?;
|
||||
let envelope = serde_json::from_slice(&json).map_err(error::envelope)?;
|
||||
|
||||
Ok((envelope, eml))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E: Executor> Clone for AsyncFileTransport<E> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use std::fs;
|
||||
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?file, "writing email to");
|
||||
fs::write(file, email).map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
let buf = serde_json::to_string(&envelope).map_err(error::envelope)?;
|
||||
fs::write(file, buf).map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[async_trait]
|
||||
impl<E> AsyncTransport for AsyncFileTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.inner.path(&email_id, "eml");
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(?file, "writing email to");
|
||||
E::fs_write(&file, email).await.map_err(error::io)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.inner.save_envelope {
|
||||
let file = self.inner.path(&email_id, "json");
|
||||
let buf = serde_json::to_vec(&envelope).map_err(error::envelope)?;
|
||||
E::fs_write(&file, &buf).await.map_err(error::io)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,177 @@
|
||||
//! Represents an Email transport
|
||||
pub mod smtp;
|
||||
pub mod error;
|
||||
pub mod stub;
|
||||
//! ## Transports for sending emails
|
||||
//!
|
||||
//! This module contains `Transport`s for sending emails. A `Transport` implements a high-level API
|
||||
//! for sending emails. It automatically manages the underlying resources and doesn't require any
|
||||
//! specific knowledge of email protocols in order to be used.
|
||||
//!
|
||||
//! ### Getting started
|
||||
//!
|
||||
//! Sending emails from your programs requires using an email relay, as client libraries are not
|
||||
//! designed to handle email delivery by themselves. Depending on your infrastructure, your relay
|
||||
//! could be:
|
||||
//!
|
||||
//! * a service from your Cloud or hosting provider
|
||||
//! * an email server ([MTA] for Mail Transfer Agent, like Postfix or Exchange), running either
|
||||
//! locally on your servers or accessible over the network
|
||||
//! * a dedicated external service, like Mailchimp, Mailgun, etc.
|
||||
//!
|
||||
//! In most cases, the best option is to:
|
||||
//!
|
||||
//! * Use the [`SMTP`] transport, with the [`relay`] builder (or one of its async counterparts)
|
||||
//! with your server's hostname. They provide modern and secure defaults.
|
||||
//! * Use the [`credentials`] method of the builder to pass your credentials.
|
||||
//!
|
||||
//! These should be enough to safely cover most use cases.
|
||||
//!
|
||||
//! ### Available transports
|
||||
//!
|
||||
//! The following transports are available:
|
||||
//!
|
||||
//! | Module | Protocol | Sync API | Async API | Description |
|
||||
//! | ------------ | -------- | --------------------- | -------------------------- | ------------------------------------------------------- |
|
||||
//! | [`smtp`] | SMTP | [`SmtpTransport`] | [`AsyncSmtpTransport`] | Uses the SMTP protocol to send emails to a relay server |
|
||||
//! | [`sendmail`] | Sendmail | [`SendmailTransport`] | [`AsyncSendmailTransport`] | Uses the `sendmail` command to send emails |
|
||||
//! | [`file`] | File | [`FileTransport`] | [`AsyncFileTransport`] | Saves the email as an `.eml` file |
|
||||
//! | [`stub`] | Debug | [`StubTransport`] | [`AsyncStubTransport`] | Drops the email - Useful for debugging |
|
||||
//!
|
||||
//! ## Building an email
|
||||
//!
|
||||
//! Emails can either be built though [`Message`], which is a typed API for constructing emails
|
||||
//! (find out more about it by going over the [`message`][crate::message] module),
|
||||
//! or via external means.
|
||||
//!
|
||||
//! [`Message`]s can be sent via [`Transport::send`] or [`AsyncTransport::send`], while messages
|
||||
//! built without lettre's [`message`][crate::message] APIs can be sent via [`Transport::send_raw`]
|
||||
//! or [`AsyncTransport::send_raw`].
|
||||
//!
|
||||
//! ## Brief example
|
||||
//!
|
||||
//! This example shows how to build an email and send it via an SMTP relay server.
|
||||
//! It is in no way a complete example, but it shows how to get started with lettre.
|
||||
//! More examples can be found by looking at the specific modules, linked in the _Module_ column
|
||||
//! of the [table above](#transports-for-sending-emails).
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "builder", feature = "smtp-transport"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
//! SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let creds = Credentials::new("smtp_username".to_owned(), "smtp_password".to_owned());
|
||||
//!
|
||||
//! // Open a remote connection to the SMTP relay server
|
||||
//! let mailer = SmtpTransport::relay("smtp.gmail.com")?
|
||||
//! .credentials(creds)
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email
|
||||
//! match mailer.send(&email) {
|
||||
//! Ok(_) => println!("Email sent successfully!"),
|
||||
//! Err(e) => panic!("Could not send email: {e:?}"),
|
||||
//! }
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # #[cfg(not(all(feature = "builder", feature = "smtp-transport")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! [MTA]: https://en.wikipedia.org/wiki/Message_transfer_agent
|
||||
//! [`SMTP`]: crate::transport::smtp
|
||||
//! [`relay`]: crate::SmtpTransport::relay
|
||||
//! [`starttls_relay`]: crate::SmtpTransport::starttls_relay
|
||||
//! [`credentials`]: crate::transport::smtp::SmtpTransportBuilder::credentials
|
||||
//! [`Message`]: crate::Message
|
||||
//! [`file`]: self::file
|
||||
//! [`SmtpTransport`]: crate::SmtpTransport
|
||||
//! [`AsyncSmtpTransport`]: crate::AsyncSmtpTransport
|
||||
//! [`SendmailTransport`]: crate::SendmailTransport
|
||||
//! [`AsyncSendmailTransport`]: crate::AsyncSendmailTransport
|
||||
//! [`FileTransport`]: crate::FileTransport
|
||||
//! [`AsyncFileTransport`]: crate::AsyncFileTransport
|
||||
//! [`StubTransport`]: crate::transport::stub::StubTransport
|
||||
//! [`AsyncStubTransport`]: crate::transport::stub::AsyncStubTransport
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::Envelope;
|
||||
#[cfg(feature = "builder")]
|
||||
use crate::Message;
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use email::SendableEmail;
|
||||
/// Blocking Transport method for emails
|
||||
pub trait Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait EmailTransport {
|
||||
/// Sends the email
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult;
|
||||
/// Close the transport explicitely
|
||||
fn close(&mut self);
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("starting to send an email");
|
||||
|
||||
let raw = message.formatted();
|
||||
self.send_raw(message.envelope(), &raw)
|
||||
}
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
/// Shuts down the transport. Future calls to [`Self::send`] and
|
||||
/// [`Self::send_raw`] might fail.
|
||||
fn shutdown(&self) {}
|
||||
}
|
||||
|
||||
/// Async Transport method for emails
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
#[async_trait]
|
||||
pub trait AsyncTransport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("starting to send an email");
|
||||
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
|
||||
/// Shuts down the transport. Future calls to [`Self::send`] and
|
||||
/// [`Self::send_raw`] might fail.
|
||||
async fn shutdown(&self) {}
|
||||
}
|
||||
|
||||
93
src/transport/sendmail/error.rs
Normal file
93
src/transport/sendmail/error.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
use crate::BoxError;
|
||||
|
||||
/// The Errors that may occur when sending an email over sendmail
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error comes from the response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::sendmail::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
326
src/transport/sendmail/mod.rs
Normal file
326
src/transport/sendmail/mod.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
//! The sendmail transport sends the email using the local `sendmail` command.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{message::header::ContentType, Message, SendmailTransport, Transport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "sendmail-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, AsyncSendmailTransport, AsyncTransport, Message,
|
||||
//! SendmailTransport, Tokio1Executor,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<Tokio1Executor>::new();
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x example
|
||||
//!
|
||||
//!```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//! #
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncTransport, AsyncStd1Executor,message::header::ContentType, AsyncSendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year").header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = AsyncSendmailTransport::<AsyncStd1Executor>::new();
|
||||
//! sender.send(email).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use std::marker::PhantomData;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
pub use self::error::Error;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{address::Envelope, Transport};
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
use crate::{AsyncTransport, Executor};
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAULT_SENDMAIL: &str = "sendmail";
|
||||
|
||||
/// Sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub struct SendmailTransport {
|
||||
command: OsString,
|
||||
}
|
||||
|
||||
/// Asynchronously sends emails using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSendmailTransport<E: Executor> {
|
||||
inner: SendmailTransport,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [`SendmailTransport::new_with_command`].
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAULT_SENDMAIL.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<OsString>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn command(&self, envelope: &Envelope) -> Command {
|
||||
let mut c = Command::new(&self.command);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new transport with the `sendmail` command
|
||||
///
|
||||
/// Note: This uses the `sendmail` command in the current `PATH`. To use another command,
|
||||
/// use [`AsyncSendmailTransport::new_with_command`].
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<OsString>>(command: S) -> Self {
|
||||
Self {
|
||||
inner: SendmailTransport::new_with_command(command),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
|
||||
use tokio1_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
fn async_std_command(&self, envelope: &Envelope) -> async_std::process::Command {
|
||||
use async_std::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.inner.command);
|
||||
// TODO: figure out why enabling this kills it earlier
|
||||
// c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SendmailTransport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl<E> Default for AsyncSendmailTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(command = ?self.command, "sending email with");
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = self.command(envelope).spawn().map_err(error::client)?;
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<AsyncStd1Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use async_std::io::prelude::WriteExt;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||
|
||||
let mut command = self.async_std_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSendmailTransport<Tokio1Executor> {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio1_crate::io::AsyncWriteExt;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(command = ?self.inner.command, "sending email with");
|
||||
|
||||
let mut command = self.tokio1_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn().map_err(error::client)?;
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(email)
|
||||
.await
|
||||
.map_err(error::client)?;
|
||||
let output = process.wait_with_output().await.map_err(error::client)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let stderr = String::from_utf8(output.stderr).map_err(error::response)?;
|
||||
Err(error::client(stderr))
|
||||
}
|
||||
}
|
||||
}
|
||||
522
src/transport/smtp/async_transport.rs
Normal file
522
src/transport/smtp/async_transport.rs
Normal file
@@ -0,0 +1,522 @@
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
marker::PhantomData,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::async_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
use super::Tls;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Executor;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Executor;
|
||||
use crate::{Envelope, Executor};
|
||||
|
||||
/// Asynchronously sends emails using the SMTP protocol
|
||||
///
|
||||
/// `AsyncSmtpTransport` is the primary way for communicating
|
||||
/// with SMTP relay servers to send email messages. It holds the
|
||||
/// client connect configuration and creates new connections
|
||||
/// as necessary.
|
||||
///
|
||||
/// # Connection pool
|
||||
///
|
||||
/// When the `pool` feature is enabled (default), `AsyncSmtpTransport` maintains a
|
||||
/// connection pool to manage SMTP connections. The pool:
|
||||
///
|
||||
/// - Establishes a new connection when sending a message.
|
||||
/// - Recycles connections internally after a message is sent.
|
||||
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
|
||||
///
|
||||
/// The connection pool can grow to hold multiple SMTP connections if multiple
|
||||
/// emails are sent concurrently, as SMTP does not support multiplexing within a
|
||||
/// single connection.
|
||||
///
|
||||
/// However, **connection reuse is not possible** if the `SyncSmtpTransport` instance
|
||||
/// is dropped after every email send operation. You must reuse the instance
|
||||
/// of this struct for the connection pool to be of any use.
|
||||
///
|
||||
/// To customize connection pool settings, use [`AsyncSmtpTransportBuilder::pool_config`].
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransport<E: Executor> {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool<E>>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: AsyncSmtpClient<E>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<Tokio1Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.abort().await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncSmtpTransport<AsyncStd1Executor> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> AsyncSmtpTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{Tls, TlsParameters, SUBMISSION_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> AsyncSmtpTransport<E> {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
AsyncSmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
/// Creates a `AsyncSmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||
/// in a single URL. This may be simpler than having to configure SMTP
|
||||
/// through multiple configuration parameters and then having to pass
|
||||
/// those options to lettre.
|
||||
///
|
||||
/// The URL is created in the following way:
|
||||
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||
///
|
||||
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||
/// SMTP relay doesn't require authentication. When `port` is not
|
||||
/// configured it is automatically determined based on the `scheme`.
|
||||
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||
/// or `required` (require the server to upgrade the connection to
|
||||
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||
///
|
||||
/// Use the following table to construct your SMTP url:
|
||||
///
|
||||
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||
///
|
||||
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||
/// be concatenated to construct the final URL because special characters
|
||||
/// contained within the parameter may confuse the URL decoder.
|
||||
/// Manually URL encode the parameters before concatenating them or use
|
||||
/// a proper URL encoder, like the following cargo script:
|
||||
///
|
||||
/// ```rust
|
||||
/// # const TOML: &str = r#"
|
||||
/// #!/usr/bin/env cargo
|
||||
///
|
||||
/// //! ```cargo
|
||||
/// //! [dependencies]
|
||||
/// //! url = "2"
|
||||
/// //! ```
|
||||
/// # "#;
|
||||
///
|
||||
/// use url::Url;
|
||||
///
|
||||
/// fn main() {
|
||||
/// // don't touch this line
|
||||
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||
///
|
||||
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||
/// url.set_scheme("smtps").unwrap();
|
||||
/// // configure the username and password.
|
||||
/// // remove the following two lines if unauthenticated.
|
||||
/// url.set_username("username").unwrap();
|
||||
/// url.set_password(Some("password")).unwrap();
|
||||
/// // configure the hostname
|
||||
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||
/// // configure the port - only necessary if using a non-default port
|
||||
/// url.set_port(Some(465)).unwrap();
|
||||
/// // configure the EHLO name
|
||||
/// url.set_path("ehlo-name");
|
||||
///
|
||||
/// println!("{url}");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The connection URL can then be used in the following way:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
/// message::header::ContentType, transport::smtp::authentication::Credentials,
|
||||
/// AsyncSmtpTransport, AsyncTransport, Message, Tokio1Executor,
|
||||
/// };
|
||||
/// # use tokio1_crate as tokio;
|
||||
///
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let email = Message::builder()
|
||||
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
/// .subject("Happy new year")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Be happy!"))
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to gmail
|
||||
/// let mailer: AsyncSmtpTransport<Tokio1Executor> =
|
||||
/// AsyncSmtpTransport::<Tokio1Executor>::from_url(
|
||||
/// "smtps://username:password@smtp.example.com:465",
|
||||
/// )?
|
||||
/// .build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// mailer.send(email).await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterward if a connection pool is not used.
|
||||
pub async fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let is_connected = conn.test_connected().await;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Debug for AsyncSmtpTransport<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Clone for AsyncSmtpTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc::clone(&self.inner),
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
impl AsyncSmtpTransportBuilder {
|
||||
// Create new builder with default parameters
|
||||
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||
let info = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
AsyncSmtpTransportBuilder {
|
||||
info,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// lettre usually picks the correct `port` when building
|
||||
/// [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect `port` and [`Self::tls`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// By default lettre chooses the correct `tls` configuration when
|
||||
/// building [`AsyncSmtpTransport`] using [`AsyncSmtpTransport::relay`] or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect [`Tls`] and [`Self::port`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "async-std1-rustls"
|
||||
)))
|
||||
)]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
pub fn build<E>(self) -> AsyncSmtpTransport<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
let client = AsyncSmtpClient {
|
||||
info: self.info,
|
||||
marker_: PhantomData,
|
||||
};
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
pub(super) struct AsyncSmtpClient<E> {
|
||||
info: SmtpInfo,
|
||||
marker_: PhantomData<E>,
|
||||
}
|
||||
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub(super) async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
||||
let mut conn = E::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
&self.info.tls,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, credentials).await?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Debug for AsyncSmtpClient<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("AsyncSmtpClient");
|
||||
builder.field("info", &self.info);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
// `clone` is unused when the `pool` feature is on
|
||||
#[allow(dead_code)]
|
||||
impl<E> AsyncSmtpClient<E>
|
||||
where
|
||||
E: Executor,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
info: self.info.clone(),
|
||||
marker_: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,112 +1,211 @@
|
||||
//! Provides authentication mechanisms
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use rustc_serialize::base64::{self, FromBase64, ToBase64};
|
||||
use rustc_serialize::hex::ToHex;
|
||||
use crypto::hmac::Hmac;
|
||||
use crypto::md5::Md5;
|
||||
use crypto::mac::Mac;
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
|
||||
use transport::smtp::NUL;
|
||||
use transport::error::Error;
|
||||
/// Accepted authentication mechanisms
|
||||
///
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Create a `Credentials` struct from username and password
|
||||
pub fn new(username: String, password: String) -> Credentials {
|
||||
Credentials {
|
||||
authentication_identity: username,
|
||||
secret: password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> From<(S, T)> for Credentials
|
||||
where
|
||||
S: Into<String>,
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from((username, password): (S, T)) -> Self {
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Credentials {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Credentials").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
/// PLAIN authentication mechanism, defined in
|
||||
/// [RFC 4616](https://tools.ietf.org/html/rfc4616)
|
||||
Plain,
|
||||
/// CRAM-MD5 authentication mechanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5,
|
||||
/// LOGIN authentication mechanism
|
||||
/// Obsolete but needed for some providers (like Office 365)
|
||||
///
|
||||
/// Defined in [draft-murchison-sasl-login-00](https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt).
|
||||
Login,
|
||||
/// Non-standard XOAUTH2 mechanism, defined in
|
||||
/// [xoauth2-protocol](https://developers.google.com/gmail/imap/xoauth2-protocol)
|
||||
Xoauth2,
|
||||
}
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::CramMd5 => "CRAM-MD5",
|
||||
})
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mechanism::Plain => true,
|
||||
Mechanism::CramMd5 => false,
|
||||
/// Does the mechanism support initial response?
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||
Mechanism::Login => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string to send to the server, using the provided username, password and
|
||||
/// challenge in some cases
|
||||
pub fn response(&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
challenge: Option<&str>)
|
||||
-> Result<String, Error> {
|
||||
match *self {
|
||||
Mechanism::Plain => {
|
||||
match challenge {
|
||||
Some(_) =>
|
||||
Err(Error::ClientError("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD)),
|
||||
pub fn response(
|
||||
self,
|
||||
credentials: &Credentials,
|
||||
challenge: Option<&str>,
|
||||
) -> Result<String, Error> {
|
||||
match self {
|
||||
Mechanism::Plain => match challenge {
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"\u{0}{}\u{0}{}",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
Mechanism::Login => {
|
||||
let decoded_challenge = challenge
|
||||
.ok_or_else(|| error::client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["User Name", "Username:", "Username", "User Name\0"],
|
||||
) {
|
||||
return Ok(credentials.authentication_identity.clone());
|
||||
}
|
||||
|
||||
if contains_ignore_ascii_case(
|
||||
decoded_challenge,
|
||||
["Password", "Password:", "Password\0"],
|
||||
) {
|
||||
return Ok(credentials.secret.clone());
|
||||
}
|
||||
|
||||
Err(error::client("Unrecognized challenge"))
|
||||
}
|
||||
Mechanism::CramMd5 => {
|
||||
let encoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None =>
|
||||
return Err(Error::ClientError("This mechanism does expect a challenge")),
|
||||
};
|
||||
|
||||
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||
Ok(challenge) => challenge,
|
||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||
};
|
||||
|
||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||
hmac.input(&decoded_challenge);
|
||||
|
||||
Ok(format!("{} {}", username, hmac.result().code().to_hex())
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD))
|
||||
}
|
||||
Mechanism::Xoauth2 => match challenge {
|
||||
Some(_) => Err(error::client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"user={}\x01auth=Bearer {}\x01\x01",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn contains_ignore_ascii_case<'a>(
|
||||
haystack: &str,
|
||||
needles: impl IntoIterator<Item = &'a str>,
|
||||
) -> bool {
|
||||
needles
|
||||
.into_iter()
|
||||
.any(|item| item.eq_ignore_ascii_case(haystack))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mechanism;
|
||||
use super::{Credentials, Mechanism};
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
assert_eq!(mechanism.response("username", "password", None).unwrap(),
|
||||
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
assert!(mechanism.response("username", "password", Some("test")).is_err());
|
||||
let credentials = Credentials::new("username".to_owned(), "password".to_owned());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"\u{0}username\u{0}password"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
let mechanism = Mechanism::CramMd5;
|
||||
fn test_login() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
assert_eq!(mechanism.response("alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=\
|
||||
="))
|
||||
.unwrap(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
assert!(mechanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||
assert!(mechanism.response("alice", "wonderland", None).is_err());
|
||||
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Password")).unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login_case_insensitive() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
let credentials = Credentials::new("alice".to_owned(), "wonderland".to_owned());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("username")).unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("password")).unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xoauth2() {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
let credentials = Credentials::new(
|
||||
"username".to_owned(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_user_pass_for_credentials() {
|
||||
assert_eq!(
|
||||
Credentials::new("alice".to_owned(), "wonderland".to_owned()),
|
||||
Credentials::from(("alice", "wonderland"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
413
src/transport/smtp/client/async_connection.rs
Normal file
413
src/transport/smtp/client/async_connection.rs
Normal file
@@ -0,0 +1,413 @@
|
||||
use std::{fmt::Display, net::IpAddr, time::Duration};
|
||||
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
use super::async_net::AsyncTokioStream;
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
#[allow(deprecated)]
|
||||
use super::{async_net::AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort().await;
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct AsyncSmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
#[allow(deprecated)]
|
||||
stream: BufReader<AsyncNetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl AsyncSmtpConnection {
|
||||
/// Get information about the server
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Connects with existing async stream
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_with_transport(
|
||||
stream: Box<dyn AsyncTokioStream>,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream = AsyncNetworkStream::use_existing_tokio1(stream);
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// If `tls_parameters` is `Some`, then the connection will use Implicit TLS (sometimes
|
||||
/// referred to as `SMTPS`). See also [`AsyncSmtpConnection::starttls`].
|
||||
///
|
||||
/// If `local_address` is `Some`, then the address provided shall be used to bind the
|
||||
/// connection to a specific local address using [`tokio1_crate::net::TcpSocket::bind`].
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::time::Duration;
|
||||
/// # use lettre::transport::smtp::{client::{AsyncSmtpConnection, TlsParameters}, extension::ClientId};
|
||||
/// # use tokio1_crate::{self as tokio, net::ToSocketAddrs as _};
|
||||
/// #
|
||||
/// # #[tokio::main]
|
||||
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let connection = AsyncSmtpConnection::connect_tokio1(
|
||||
/// ("example.com", 465),
|
||||
/// Some(Duration::from_secs(60)),
|
||||
/// &ClientId::default(),
|
||||
/// Some(TlsParameters::new("example.com".to_owned())?),
|
||||
/// None,
|
||||
/// )
|
||||
/// .await?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_tokio1<T: tokio1_crate::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
local_address: Option<IpAddr>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream =
|
||||
AsyncNetworkStream::connect_tokio1(server, timeout, tls_parameters, local_address)
|
||||
.await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
pub async fn connect_asyncstd1<T: async_std::net::ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(deprecated)]
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(server, timeout, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
async fn connect_impl(
|
||||
stream: AsyncNetworkStream,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = AsyncSmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
// TODO log
|
||||
let _response = conn.read_response().await?;
|
||||
|
||||
conn.ehlo(hello_name).await?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
// Internationalization handling
|
||||
//
|
||||
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
|
||||
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
|
||||
|
||||
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in the message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await,
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(
|
||||
self.command(Rcpt::new(to_address.clone(), vec![])).await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data).await, self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email).await, self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
/// Upgrade the connection using `STARTTLS`.
|
||||
///
|
||||
/// As described in [rfc3207]. Note that this mechanism has been deprecated in [rfc8314].
|
||||
///
|
||||
/// [rfc3207]: https://www.rfc-editor.org/rfc/rfc3207
|
||||
/// [rfc8314]: https://www.rfc-editor.org/rfc/rfc8314
|
||||
#[allow(unused_variables)]
|
||||
pub async fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit).await, self))
|
||||
}
|
||||
|
||||
pub async fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit).await;
|
||||
}
|
||||
let _ = self.stream.close().await;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
#[allow(deprecated)]
|
||||
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub async fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).await.is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||
pub async fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges: u8 = 10;
|
||||
let mut response = self
|
||||
.command(Auth::new(mechanism, credentials.clone(), None)?)
|
||||
.await?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)
|
||||
.await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub async fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice()).await?;
|
||||
self.write(b"\r\n.\r\n").await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub async fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes()).await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
self.stream
|
||||
.get_mut()
|
||||
.flush()
|
||||
.await
|
||||
.map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub async fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self
|
||||
.stream
|
||||
.read_line(&mut buffer)
|
||||
.await
|
||||
.map_err(error::network)?
|
||||
> 0
|
||||
{
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(
|
||||
response.code(),
|
||||
Some(response.message().collect()),
|
||||
))
|
||||
}
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
|
||||
/// Currently this is only avaialable when using Boring TLS and
|
||||
/// returns the result of the verification of the TLS certificate
|
||||
/// presented by the peer, if any. Only the last error encountered
|
||||
/// during verification is presented.
|
||||
/// It can be useful when you don't want to fail outright the TLS
|
||||
/// negotiation, for example when a self-signed certificate is
|
||||
/// encountered, but still want to record metrics or log the fact.
|
||||
/// When using DANE verification, the PKI root of trust moves from
|
||||
/// the CAs to DNS, so self-signed certificates are permitted as long
|
||||
/// as the TLSA records match the leaf or issuer certificates.
|
||||
/// It cannot be called on non Boring TLS streams.
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
self.stream.get_ref().tls_verify_result()
|
||||
}
|
||||
|
||||
/// All the X509 certificates of the chain (DER encoded)
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
self.stream.get_ref().certificate_chain()
|
||||
}
|
||||
}
|
||||
667
src/transport/smtp/client/async_net.rs
Normal file
667
src/transport/smtp/client/async_net.rs
Normal file
@@ -0,0 +1,667 @@
|
||||
use std::{
|
||||
fmt, io, mem,
|
||||
net::{IpAddr, SocketAddr},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::{TcpStream as AsyncStd1TcpStream, ToSocketAddrs as AsyncStd1ToSocketAddrs};
|
||||
use futures_io::{
|
||||
AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite, Error as IoError,
|
||||
Result as IoResult,
|
||||
};
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
use futures_rustls::client::TlsStream as AsyncStd1RustlsStream;
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
use tokio1_boring::SslStream as Tokio1SslStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::io::{AsyncRead, AsyncWrite, ReadBuf as Tokio1ReadBuf};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::net::{
|
||||
TcpSocket as Tokio1TcpSocket, TcpStream as Tokio1TcpStream,
|
||||
ToSocketAddrs as Tokio1ToSocketAddrs,
|
||||
};
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsStream;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls",
|
||||
feature = "async-std1-rustls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::transport::smtp::client::net::resolved_address_filter;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A network stream
|
||||
#[derive(Debug)]
|
||||
#[deprecated(
|
||||
since = "0.11.14",
|
||||
note = "This struct was not meant to be made public"
|
||||
)]
|
||||
pub struct AsyncNetworkStream {
|
||||
inner: InnerAsyncNetworkStream,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub trait AsyncTokioStream: AsyncRead + AsyncWrite + Send + Sync + Unpin + fmt::Debug {
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl AsyncTokioStream for Tokio1TcpStream {
|
||||
fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
self.peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
enum InnerAsyncNetworkStream {
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1")]
|
||||
Tokio1Tcp(Box<dyn AsyncTokioStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
Tokio1NativeTls(Tokio1TlsStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
Tokio1Rustls(Tokio1RustlsStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
Tokio1BoringTls(Tokio1SslStream<Box<dyn AsyncTokioStream>>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1")]
|
||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
AsyncStd1Rustls(AsyncStd1RustlsStream<AsyncStd1TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
AsyncNetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::other(
|
||||
"InnerAsyncNetworkStream::None must never be built",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub fn use_existing_tokio1(stream: Box<dyn AsyncTokioStream>) -> AsyncNetworkStream {
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(stream))
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub async fn connect_tokio1<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
async fn try_connect<T: Tokio1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<Tokio1TcpStream, Error> {
|
||||
let addrs = tokio1_crate::net::lookup_host(server)
|
||||
.await
|
||||
.map_err(error::connection)?
|
||||
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let socket = match addr.ip() {
|
||||
IpAddr::V4(_) => Tokio1TcpSocket::new_v4(),
|
||||
IpAddr::V6(_) => Tokio1TcpSocket::new_v6(),
|
||||
}
|
||||
.map_err(error::connection)?;
|
||||
if let Some(local_addr) = local_addr {
|
||||
socket
|
||||
.bind(SocketAddr::new(local_addr, 0))
|
||||
.map_err(error::connection)?;
|
||||
}
|
||||
|
||||
let connect_future = socket.connect(addr);
|
||||
if let Some(timeout) = timeout {
|
||||
match tokio1_crate::time::timeout(timeout, connect_future).await {
|
||||
Ok(Ok(stream)) => return Ok(stream),
|
||||
Ok(Err(err)) => last_err = Some(err),
|
||||
Err(_) => {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match connect_future.await {
|
||||
Ok(stream) => return Ok(stream),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any supported address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = try_connect(server, timeout, local_addr).await?;
|
||||
let mut stream =
|
||||
AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(Box::new(tcp_stream)));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
pub async fn connect_asyncstd1<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
// Unfortunately, there doesn't currently seem to be a way to set the local address.
|
||||
// Whilst we can create a AsyncStd1TcpStream from an existing socket, it needs to first have
|
||||
// been connected, which is a blocking operation.
|
||||
async fn try_connect_timeout<T: AsyncStd1ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<AsyncStd1TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs().await.map_err(error::connection)?;
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let connect_future = AsyncStd1TcpStream::connect(&addr);
|
||||
match async_std::future::timeout(timeout, connect_future).await {
|
||||
Ok(Ok(stream)) => return Ok(stream),
|
||||
Ok(Err(err)) => last_err = Some(err),
|
||||
Err(_) => {
|
||||
last_err = Some(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"connection timed out",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t).await?,
|
||||
None => AsyncStd1TcpStream::connect(server)
|
||||
.await
|
||||
.map_err(error::connection)?,
|
||||
};
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
not(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(feature = "async-std1", not(feature = "async-std1-rustls")))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled the async-std1-rustls feature");
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls",
|
||||
feature = "tokio1-boring-tls"
|
||||
))]
|
||||
async fn upgrade_tokio1_tls(
|
||||
tcp_stream: Box<dyn AsyncTokioStream>,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
match tls_parameters.inner {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(inner) => {
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
return {
|
||||
use tokio1_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
let stream = connector
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(inner) => {
|
||||
#[cfg(not(feature = "tokio1-rustls"))]
|
||||
panic!("built without the tokio1-rustls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
return {
|
||||
use tokio1_rustls::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
let stream = connector
|
||||
.connect(inner.server_name.inner(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(inner) => {
|
||||
#[cfg(not(feature = "tokio1-boring-tls"))]
|
||||
panic!("built without the tokio1-boring-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
return {
|
||||
let mut config = inner.connector.configure().map_err(error::connection)?;
|
||||
config.set_verify_hostname(inner.extra_info.accept_invalid_hostnames);
|
||||
|
||||
let stream = tokio1_boring::connect(config, &inner.server_name, tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1BoringTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
async fn upgrade_asyncstd1_tls(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
match tls_parameters.inner {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(_) => {
|
||||
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(inner) => {
|
||||
#[cfg(not(feature = "async-std1-rustls"))]
|
||||
panic!("built without the async-std1-rustls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
return {
|
||||
use futures_rustls::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(inner.connector);
|
||||
let stream = connector
|
||||
.connect(inner.server_name.inner(), tcp_stream)
|
||||
.await
|
||||
.map_err(error::connection)?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1Rustls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(_inner) => {
|
||||
panic!("boring-tls isn't supported with async-std yet.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(_) => true,
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(_) => true,
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => true,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => {
|
||||
stream.ssl().verify_result().map_err(error::tls)
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(_) => panic!("Unsupported"),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.to_vec())
|
||||
.collect()),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
.peer_cert_chain()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.to_der().map_err(error::tls))
|
||||
.collect::<Result<Vec<_>, _>>()?),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.to_vec())
|
||||
.collect()),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
match &self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.peer_certificate()
|
||||
.map_err(error::tls)?
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
.peer_certificate()
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
Err(error::client("Connection is not encrypted"))
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(stream) => Ok(stream
|
||||
.get_ref()
|
||||
.1
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
InnerAsyncNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_read(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_write(cx, buf),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match &mut self.inner {
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls")]
|
||||
InnerAsyncNetworkStream::Tokio1Rustls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-boring-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1BoringTls(s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Rustls(s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
334
src/transport/smtp/client/connection.rs
Normal file
334
src/transport/smtp/client/connection.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::{IpAddr, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
|
||||
error,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
};
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort();
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct SmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<NetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl SmtpConnection {
|
||||
/// Get information about the server
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
// FIXME add simple connect and rename this one
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
server: A,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
local_address: Option<IpAddr>,
|
||||
) -> Result<SmtpConnection, Error> {
|
||||
let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = SmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout).map_err(error::network)?;
|
||||
// TODO log
|
||||
let _response = conn.read_response()?;
|
||||
|
||||
conn.ehlo(hello_name)?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
// Internationalization handling
|
||||
//
|
||||
// * 8BITMIME: https://tools.ietf.org/html/rfc6152
|
||||
// * SMTPUTF8: https://tools.ietf.org/html/rfc653
|
||||
|
||||
// Check for non-ascii addresses and use the SMTPUTF8 option if any.
|
||||
if envelope.has_non_ascii_addresses() {
|
||||
if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
|
||||
// don't try to send non-ascii addresses (per RFC)
|
||||
return Err(error::client(
|
||||
"Envelope contains non-ascii chars but server does not support SMTPUTF8",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
// Check for non-ascii content in the message
|
||||
if !email.is_ascii() {
|
||||
if !self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
return Err(error::client(
|
||||
"Message contains non-ascii chars but server does not support 8BITMIME",
|
||||
));
|
||||
}
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options)),
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data), self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email), self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: &TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||
// This should never happen as `Tls` can only be created
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
} else {
|
||||
Err(error::client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit), self))
|
||||
}
|
||||
|
||||
pub fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit);
|
||||
}
|
||||
let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: NetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
self.stream.get_mut().set_read_timeout(duration)?;
|
||||
self.stream.get_mut().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles the challenge if needed
|
||||
pub fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?),
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(error::response("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut out_buf = Vec::with_capacity(message.len());
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream
|
||||
.get_mut()
|
||||
.write_all(string)
|
||||
.map_err(error::network)?;
|
||||
self.stream.get_mut().flush().map_err(error::network)?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
return if response.is_positive() {
|
||||
Ok(response)
|
||||
} else {
|
||||
Err(error::code(
|
||||
response.code(),
|
||||
Some(response.message().collect()),
|
||||
))
|
||||
};
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(error::response(e.to_string()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(error::response("incomplete response"))
|
||||
}
|
||||
|
||||
/// The X509 certificate of the server (DER encoded)
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
self.stream.get_ref().peer_certificate()
|
||||
}
|
||||
|
||||
/// Currently this is only avaialable when using Boring TLS and
|
||||
/// returns the result of the verification of the TLS certificate
|
||||
/// presented by the peer, if any. Only the last error encountered
|
||||
/// during verification is presented.
|
||||
/// It can be useful when you don't want to fail outright the TLS
|
||||
/// negotiation, for example when a self-signed certificate is
|
||||
/// encountered, but still want to record metrics or log the fact.
|
||||
/// When using DANE verification, the PKI root of trust moves from
|
||||
/// the CAs to DNS, so self-signed certificates are permitted as long
|
||||
/// as the TLSA records match the leaf or issuer certificates.
|
||||
/// It cannot be called on non Boring TLS streams.
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
self.stream.get_ref().tls_verify_result()
|
||||
}
|
||||
|
||||
/// All the X509 certificates of the chain (DER encoded)
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
self.stream.get_ref().certificate_chain()
|
||||
}
|
||||
}
|
||||
@@ -1,277 +1,154 @@
|
||||
//! SMTP client
|
||||
//!
|
||||
//! `SmtpConnection` allows manually sending SMTP commands.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::transport::smtp::{
|
||||
//! client::SmtpConnection, commands::*, extension::ClientId, SMTP_PORT,
|
||||
//! };
|
||||
//!
|
||||
//! let hello = ClientId::Domain("my_hostname".to_owned());
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None, None)?;
|
||||
//! client.command(Mail::new(Some("user@example.com".parse()?), vec![]))?;
|
||||
//! client.command(Rcpt::new("user@example.org".parse()?, vec![]))?;
|
||||
//! client.command(Data)?;
|
||||
//! client.message("Test email".as_bytes())?;
|
||||
//! client.command(Quit)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::string::String;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::io;
|
||||
use std::io::{BufRead, Read, Write};
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
|
||||
use bufstream::BufStream;
|
||||
use openssl::ssl::SslContext;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[allow(deprecated)]
|
||||
pub use self::async_net::AsyncNetworkStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::async_net::AsyncTokioStream;
|
||||
use self::net::NetworkStream;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub(super) use self::tls::current::InnerTlsParameters;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub use self::tls::current::TlsVersion;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
tls::current::{
|
||||
Certificate, CertificateStore, Identity, Tls, TlsParameters, TlsParametersBuilder,
|
||||
},
|
||||
};
|
||||
|
||||
use transport::error::{EmailResult, Error};
|
||||
use transport::smtp::response::ResponseParser;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::smtp::client::net::{Connector, NetworkStream};
|
||||
use transport::smtp::{CRLF, MESSAGE_ENDING};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_connection;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_net;
|
||||
mod connection;
|
||||
mod net;
|
||||
mod tls;
|
||||
|
||||
pub mod net;
|
||||
/// The codec used for transparency
|
||||
#[derive(Debug)]
|
||||
struct ClientCodec {
|
||||
status: CodecStatus,
|
||||
}
|
||||
|
||||
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
||||
///
|
||||
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
|
||||
#[inline]
|
||||
fn escape_dot(string: &str) -> String {
|
||||
if string.starts_with(".") {
|
||||
format!(".{}", string)
|
||||
} else {
|
||||
string.to_string()
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
status: CodecStatus::StartOfNewLine,
|
||||
}
|
||||
}
|
||||
.replace("\r.", "\r..")
|
||||
.replace("\n.", "\n..")
|
||||
|
||||
/// Adds transparency
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
||||
for &b in frame {
|
||||
buf.push(b);
|
||||
match (b, self.status) {
|
||||
(b'\r', _) => {
|
||||
self.status = CodecStatus::StartingNewLine;
|
||||
}
|
||||
(b'\n', CodecStatus::StartingNewLine) => {
|
||||
self.status = CodecStatus::StartOfNewLine;
|
||||
}
|
||||
(_, CodecStatus::StartingNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
}
|
||||
(b'.', CodecStatus::StartOfNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
buf.push(b'.');
|
||||
}
|
||||
(_, CodecStatus::StartOfNewLine) => {
|
||||
self.status = CodecStatus::MiddleOfLine;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum CodecStatus {
|
||||
/// We are past the first character of the current line
|
||||
MiddleOfLine,
|
||||
/// We just read a `\r` character
|
||||
StartingNewLine,
|
||||
/// We are at the start of a new line
|
||||
StartOfNewLine,
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
#[inline]
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace(CRLF, "<CR><LF>")
|
||||
}
|
||||
|
||||
/// Returns the string removing all the CRLF
|
||||
#[inline]
|
||||
fn remove_crlf(string: &str) -> String {
|
||||
string.replace(CRLF, "")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
#[derive(Debug)]
|
||||
pub struct Client<S: Write + Read = NetworkStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
}
|
||||
|
||||
macro_rules! return_err (
|
||||
($err: expr, $client: ident) => ({
|
||||
return Err(From::from($err))
|
||||
})
|
||||
);
|
||||
|
||||
impl<S: Write + Read = NetworkStream> Client<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> Client<S> {
|
||||
Client { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.quit();
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: S) {
|
||||
self.stream = Some(BufStream::new(stream));
|
||||
}
|
||||
|
||||
/// Upgrades the underlying connection to SSL/TLS
|
||||
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
if self.stream.is_some() {
|
||||
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self,
|
||||
addr: &A,
|
||||
ssl_context: Option<&SslContext>)
|
||||
-> EmailResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
}
|
||||
|
||||
let mut addresses = try!(addr.to_socket_addrs());
|
||||
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(try!(Connector::connect(&server_addr, ssl_context)));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.noop().is_ok()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command(&mut self, command: &str) -> EmailResult {
|
||||
self.send_server(command, CRLF)
|
||||
}
|
||||
|
||||
/// Sends a EHLO command
|
||||
pub fn ehlo(&mut self, hostname: &str) -> EmailResult {
|
||||
self.command(&format!("EHLO {}", hostname))
|
||||
}
|
||||
|
||||
/// Sends a MAIL command
|
||||
pub fn mail(&mut self, address: &str, options: Option<&str>) -> EmailResult {
|
||||
match options {
|
||||
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
|
||||
None => self.command(&format!("MAIL FROM:<{}>", address)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RCPT command
|
||||
pub fn rcpt(&mut self, address: &str) -> EmailResult {
|
||||
self.command(&format!("RCPT TO:<{}>", address))
|
||||
}
|
||||
|
||||
/// Sends a DATA command
|
||||
pub fn data(&mut self) -> EmailResult {
|
||||
self.command("DATA")
|
||||
}
|
||||
|
||||
/// Sends a QUIT command
|
||||
pub fn quit(&mut self) -> EmailResult {
|
||||
self.command("QUIT")
|
||||
}
|
||||
|
||||
/// Sends a NOOP command
|
||||
pub fn noop(&mut self) -> EmailResult {
|
||||
self.command("NOOP")
|
||||
}
|
||||
|
||||
/// Sends a HELP command
|
||||
pub fn help(&mut self, argument: Option<&str>) -> EmailResult {
|
||||
match argument {
|
||||
Some(ref argument) => self.command(&format!("HELP {}", argument)),
|
||||
None => self.command("HELP"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a VRFY command
|
||||
pub fn vrfy(&mut self, address: &str) -> EmailResult {
|
||||
self.command(&format!("VRFY {}", address))
|
||||
}
|
||||
|
||||
/// Sends a EXPN command
|
||||
pub fn expn(&mut self, address: &str) -> EmailResult {
|
||||
self.command(&format!("EXPN {}", address))
|
||||
}
|
||||
|
||||
/// Sends a RSET command
|
||||
pub fn rset(&mut self) -> EmailResult {
|
||||
self.command("RSET")
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism
|
||||
pub fn auth(&mut self, mechanism: Mechanism, username: &str, password: &str) -> EmailResult {
|
||||
|
||||
if mechanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}",
|
||||
mechanism,
|
||||
try!(mechanism.response(username, password, None))))
|
||||
} else {
|
||||
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
||||
};
|
||||
|
||||
debug!("CRAM challenge: {}", encoded_challenge);
|
||||
|
||||
let cram_response = try!(mechanism.response(username,
|
||||
password,
|
||||
Some(&encoded_challenge)));
|
||||
|
||||
self.command(&format!("{}", cram_response))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a STARTTLS command
|
||||
pub fn starttls(&mut self) -> EmailResult {
|
||||
self.command("STARTTLS")
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message_content: &str) -> EmailResult {
|
||||
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
||||
}
|
||||
|
||||
/// Sends a string to the server and gets the response
|
||||
fn send_server(&mut self, string: &str, end: &str) -> EmailResult {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
|
||||
try!(self.stream.as_mut().unwrap().flush());
|
||||
|
||||
debug!("Wrote: {}", escape_crlf(string));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> EmailResult {
|
||||
|
||||
let mut parser = ResponseParser::new();
|
||||
|
||||
let mut line = String::new();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
|
||||
debug!("Read: {}", escape_crlf(line.as_ref()));
|
||||
|
||||
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
||||
line.clear();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
}
|
||||
|
||||
let response = try!(parser.response());
|
||||
|
||||
match response.is_positive() {
|
||||
true => Ok(response),
|
||||
false => Err(From::from(response)),
|
||||
}
|
||||
}
|
||||
/// Used for debug displays
|
||||
#[cfg(feature = "tracing")]
|
||||
pub(super) fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_crlf, escape_dot, remove_crlf};
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_escape_dot() {
|
||||
assert_eq!(escape_dot(".test"), "..test");
|
||||
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_crlf() {
|
||||
assert_eq!(remove_crlf("\r\n"), "");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_nameSIZE 42");
|
||||
fn test_codec() {
|
||||
let mut buf = Vec::new();
|
||||
let mut codec = ClientCodec::new();
|
||||
|
||||
codec.encode(b".\r\n", &mut buf);
|
||||
codec.encode(b"test\r\n", &mut buf);
|
||||
codec.encode(b"test\r\n\r\n", &mut buf);
|
||||
codec.encode(b".\r\n", &mut buf);
|
||||
codec.encode(b"\r\ntest", &mut buf);
|
||||
codec.encode(b"te\r\n.\r\nst", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
codec.encode(b"test.", &mut buf);
|
||||
codec.encode(b"test\n", &mut buf);
|
||||
codec.encode(b".test\n", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
codec.encode(b"test\r\n", &mut buf);
|
||||
codec.encode(b".test\r\n", &mut buf);
|
||||
codec.encode(b"test.\r\n", &mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"..\r\ntest\r\ntest\r\n\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntesttesttest\r\n..test\r\ntest.\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "tracing")]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
|
||||
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,431 @@
|
||||
//! A trait to represent a stream
|
||||
#[cfg(feature = "rustls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
mem,
|
||||
net::{IpAddr, Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use std::io;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::net::{SocketAddr, TcpStream};
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
#[cfg(feature = "boring-tls")]
|
||||
use boring::ssl::SslStream;
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
#[cfg(feature = "rustls")]
|
||||
use rustls::{ClientConnection, StreamOwned};
|
||||
use socket2::{Domain, Protocol, Type};
|
||||
|
||||
use openssl::ssl::{SslContext, SslStream};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::{error, Error};
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connector: Sized {
|
||||
/// Opens a connection to the given IP socket
|
||||
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
|
||||
/// Upgrades to TLS connection
|
||||
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
inner: InnerNetworkStream,
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
|
||||
let tcp_stream = try!(TcpStream::connect(addr));
|
||||
|
||||
match ssl_context {
|
||||
Some(context) => match SslStream::connect_generic(context, tcp_stream) {
|
||||
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
|
||||
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
None => Ok(NetworkStream::Plain(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
*self = match self.clone() {
|
||||
NetworkStream::Plain(stream) => match SslStream::connect_generic(ssl_context, stream) {
|
||||
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
pub enum NetworkStream {
|
||||
/// Plain TCP
|
||||
Plain(TcpStream),
|
||||
/// SSL over TCP
|
||||
Ssl(SslStream<TcpStream>),
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum InnerNetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls(StreamOwned<ClientConnection, TcpStream>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(SslStream<TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl Clone for NetworkStream {
|
||||
#[inline]
|
||||
fn clone(&self) -> NetworkStream {
|
||||
match self {
|
||||
&NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()),
|
||||
&NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()),
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
if let InnerNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
NetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NetworkStream {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NetworkStream(_)")
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<NetworkStream, Error> {
|
||||
fn try_connect<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server
|
||||
.to_socket_addrs()
|
||||
.map_err(error::connection)?
|
||||
.filter(|resolved_addr| resolved_address_filter(resolved_addr, local_addr));
|
||||
|
||||
let mut last_err = None;
|
||||
|
||||
for addr in addrs {
|
||||
let socket = socket2::Socket::new(
|
||||
Domain::for_address(addr),
|
||||
Type::STREAM,
|
||||
Some(Protocol::TCP),
|
||||
)
|
||||
.map_err(error::connection)?;
|
||||
bind_local_address(&socket, &addr, local_addr)?;
|
||||
|
||||
if let Some(timeout) = timeout {
|
||||
match socket.connect_timeout(&addr.into(), timeout) {
|
||||
Ok(()) => return Ok(socket.into()),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
} else {
|
||||
match socket.connect(&addr.into()) {
|
||||
Ok(()) => return Ok(socket.into()),
|
||||
Err(err) => last_err = Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(match last_err {
|
||||
Some(last_err) => error::connection(last_err),
|
||||
None => error::connection("could not resolve to any address"),
|
||||
})
|
||||
}
|
||||
|
||||
let tcp_stream = try_connect(server, timeout, local_addr)?;
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters)?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the `native-tls` or the `rustls` feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = mem::replace(&mut self.inner, InnerNetworkStream::None);
|
||||
let InnerNetworkStream::Tcp(tcp_stream) = tcp_stream else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn upgrade_tls_impl(
|
||||
tcp_stream: TcpStream,
|
||||
tls_parameters: &TlsParameters,
|
||||
) -> Result<InnerNetworkStream, Error> {
|
||||
Ok(match &tls_parameters.inner {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(inner) => {
|
||||
let stream = inner
|
||||
.connector
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(inner) => {
|
||||
let connection = ClientConnection::new(
|
||||
Arc::clone(&inner.connector),
|
||||
inner.server_name.inner_ref().clone(),
|
||||
)
|
||||
.map_err(error::connection)?;
|
||||
let stream = StreamOwned::new(connection, tcp_stream);
|
||||
InnerNetworkStream::Rustls(stream)
|
||||
}
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(inner) => {
|
||||
let stream = inner
|
||||
.connector
|
||||
.configure()
|
||||
.map_err(error::connection)?
|
||||
.verify_hostname(inner.extra_info.accept_invalid_hostnames)
|
||||
.connect(&inner.server_name, tcp_stream)
|
||||
.map_err(error::connection)?;
|
||||
InnerNetworkStream::BoringTls(stream)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(_) => true,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(_) => true,
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn tls_verify_result(&self) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => {
|
||||
stream.ssl().verify_result().map_err(error::tls)
|
||||
}
|
||||
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
|
||||
pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => panic!("Unsupported"),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => Ok(stream
|
||||
.conn
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.to_vec())
|
||||
.collect()),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
.peer_cert_chain()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|c| c.to_der().map_err(error::tls))
|
||||
.collect::<Result<Vec<_>, _>>()?),
|
||||
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
|
||||
match &self.inner {
|
||||
InnerNetworkStream::Tcp(_) => Err(error::client("Connection is not encrypted")),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => Ok(stream
|
||||
.peer_certificate()
|
||||
.map_err(error::tls)?
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => Ok(stream
|
||||
.conn
|
||||
.peer_certificates()
|
||||
.unwrap()
|
||||
.first()
|
||||
.unwrap()
|
||||
.to_vec()),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => Ok(stream
|
||||
.ssl()
|
||||
.peer_certificate()
|
||||
.unwrap()
|
||||
.to_der()
|
||||
.map_err(error::tls)?),
|
||||
InnerNetworkStream::None => panic!("InnerNetworkStream::None must never be built"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_read_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(stream) => stream.set_write_timeout(duration),
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(stream) => stream.get_ref().set_write_timeout(duration),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
#[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.read(buf),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.read(buf),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.read(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.read(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
#[inline]
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.write(msg),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.write(buf),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.write(buf),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.write(buf),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.flush(),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.flush(),
|
||||
match &mut self.inner {
|
||||
InnerNetworkStream::Tcp(s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(s) => s.flush(),
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerNetworkStream::Rustls(s) => s.flush(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerNetworkStream::BoringTls(s) => s.flush(),
|
||||
InnerNetworkStream::None => {
|
||||
debug_assert!(false, "InnerNetworkStream::None must never be built");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If the local address is set, binds the socket to this address.
|
||||
/// If local address is not set, then destination address is required to determine the default
|
||||
/// local address on some platforms.
|
||||
/// See: <https://github.com/hyperium/hyper/blob/faf24c6ad8eee1c3d5ccc9a4d4835717b8e2903f/src/client/connect/http.rs#L560>
|
||||
fn bind_local_address(
|
||||
socket: &socket2::Socket,
|
||||
dst_addr: &SocketAddr,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> Result<(), Error> {
|
||||
match local_addr {
|
||||
Some(local_addr) => {
|
||||
socket
|
||||
.bind(&SocketAddr::new(local_addr, 0).into())
|
||||
.map_err(error::connection)?;
|
||||
}
|
||||
_ => {
|
||||
if cfg!(windows) {
|
||||
// Windows requires a socket be bound before calling connect
|
||||
let any: SocketAddr = match dst_addr {
|
||||
SocketAddr::V4(_) => ([0, 0, 0, 0], 0).into(),
|
||||
SocketAddr::V6(_) => ([0, 0, 0, 0, 0, 0, 0, 0], 0).into(),
|
||||
};
|
||||
socket.bind(&any.into()).map_err(error::connection)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// When we have an iterator of resolved remote addresses, we must filter them to be the same
|
||||
/// protocol as the local address binding. If no local address is set, then all will be matched.
|
||||
pub(crate) fn resolved_address_filter(
|
||||
resolved_addr: &SocketAddr,
|
||||
local_addr: Option<IpAddr>,
|
||||
) -> bool {
|
||||
match local_addr {
|
||||
Some(local_addr) => match resolved_addr.ip() {
|
||||
IpAddr::V4(_) => local_addr.is_ipv4(),
|
||||
IpAddr::V6(_) => local_addr.is_ipv6(),
|
||||
},
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
111
src/transport/smtp/client/tls/boring_tls.rs
Normal file
111
src/transport/smtp/client/tls/boring_tls.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use boring::{
|
||||
ssl::{SslConnector, SslMethod, SslVerifyMode, SslVersion},
|
||||
x509::store::X509StoreBuilder,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
|
||||
pub(super) fn build_connector(
|
||||
builder: super::TlsParametersBuilder<super::BoringTls>,
|
||||
) -> Result<(Box<str>, SslConnector), Error> {
|
||||
let mut tls_builder = SslConnector::builder(SslMethod::tls_client()).map_err(error::tls)?;
|
||||
|
||||
if builder.accept_invalid_certs {
|
||||
tls_builder.set_verify(SslVerifyMode::NONE);
|
||||
} else {
|
||||
match builder.cert_store {
|
||||
CertificateStore::System => {}
|
||||
CertificateStore::None => {
|
||||
// Replace the default store with an empty store.
|
||||
tls_builder.set_cert_store(X509StoreBuilder::new().map_err(error::tls)?.build());
|
||||
}
|
||||
}
|
||||
|
||||
let cert_store = tls_builder.cert_store_mut();
|
||||
|
||||
for cert in builder.root_certs {
|
||||
cert_store.add_cert(cert.0).map_err(error::tls)?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(identity) = builder.identity {
|
||||
tls_builder
|
||||
.set_certificate(identity.chain.as_ref())
|
||||
.map_err(error::tls)?;
|
||||
tls_builder
|
||||
.set_private_key(identity.key.as_ref())
|
||||
.map_err(error::tls)?;
|
||||
}
|
||||
|
||||
let min_tls_version = match builder.min_tls_version {
|
||||
MinTlsVersion::Tlsv10 => SslVersion::TLS1,
|
||||
MinTlsVersion::Tlsv11 => SslVersion::TLS1_1,
|
||||
MinTlsVersion::Tlsv12 => SslVersion::TLS1_2,
|
||||
MinTlsVersion::Tlsv13 => SslVersion::TLS1_3,
|
||||
};
|
||||
|
||||
tls_builder
|
||||
.set_min_proto_version(Some(min_tls_version))
|
||||
.map_err(error::tls)?;
|
||||
Ok((builder.server_name.into_boxed_str(), tls_builder.build()))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum CertificateStore {
|
||||
#[default]
|
||||
System,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Certificate(pub(super) boring::x509::X509);
|
||||
|
||||
impl Certificate {
|
||||
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self(boring::x509::X509::from_pem(pem).map_err(error::tls)?))
|
||||
}
|
||||
|
||||
pub(super) fn from_der(der: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self(boring::x509::X509::from_der(der).map_err(error::tls)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Identity {
|
||||
pub(super) chain: boring::x509::X509,
|
||||
pub(super) key: boring::pkey::PKey<boring::pkey::Private>,
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
|
||||
let chain = boring::x509::X509::from_pem(pem).map_err(error::tls)?;
|
||||
let key = boring::pkey::PKey::private_key_from_pem(key).map_err(error::tls)?;
|
||||
Ok(Self { chain, key })
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Identity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum MinTlsVersion {
|
||||
Tlsv10,
|
||||
Tlsv11,
|
||||
#[default]
|
||||
Tlsv12,
|
||||
Tlsv13,
|
||||
}
|
||||
601
src/transport/smtp/client/tls/current.rs
Normal file
601
src/transport/smtp/client/tls/current.rs
Normal file
@@ -0,0 +1,601 @@
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use super::TlsBackend;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use crate::transport::smtp::error;
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// TLS protocol versions.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[non_exhaustive]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub enum TlsVersion {
|
||||
/// TLS 1.0
|
||||
///
|
||||
/// Should only be used when trying to support legacy
|
||||
/// SMTP servers that haven't updated to
|
||||
/// at least TLS 1.2 yet.
|
||||
///
|
||||
/// Supported by `native-tls` and `boring-tls`.
|
||||
Tlsv10,
|
||||
/// TLS 1.1
|
||||
///
|
||||
/// Should only be used when trying to support legacy
|
||||
/// SMTP servers that haven't updated to
|
||||
/// at least TLS 1.2 yet.
|
||||
///
|
||||
/// Supported by `native-tls` and `boring-tls`.
|
||||
Tlsv11,
|
||||
/// TLS 1.2
|
||||
///
|
||||
/// A good option for most SMTP servers.
|
||||
///
|
||||
/// Supported by all TLS backends.
|
||||
Tlsv12,
|
||||
/// TLS 1.3
|
||||
///
|
||||
/// The most secure option, although not supported by all SMTP servers.
|
||||
///
|
||||
/// Although it is technically supported by all TLS backends,
|
||||
/// trying to set it for `native-tls` will give a runtime error.
|
||||
Tlsv13,
|
||||
}
|
||||
|
||||
/// Specifies how to establish a TLS connection
|
||||
///
|
||||
/// TLDR: Use [`Tls::Wrapper`] or [`Tls::Required`] when
|
||||
/// connecting to a remote server, [`Tls::None`] when
|
||||
/// connecting to a local server.
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Tls` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub enum Tls {
|
||||
/// Insecure (plaintext) connection only.
|
||||
///
|
||||
/// This option **always** uses a plaintext connection and should only
|
||||
/// be used for trusted local relays. It is **highly discouraged**
|
||||
/// for remote servers, as it exposes credentials and emails to potential
|
||||
/// interception.
|
||||
///
|
||||
/// Note: Servers requiring credentials or emails to be sent over TLS
|
||||
/// may reject connections when this option is used.
|
||||
None,
|
||||
/// Begin with a plaintext connection and attempt to use `STARTTLS` if available.
|
||||
///
|
||||
/// lettre will try to upgrade to a TLS-secured connection but will fall back
|
||||
/// to plaintext if the server does not support TLS. This option is provided for
|
||||
/// compatibility but is **strongly discouraged**, as it exposes connections to
|
||||
/// potential MITM (man-in-the-middle) attacks.
|
||||
///
|
||||
/// Warning: A malicious intermediary could intercept the `STARTTLS` flag,
|
||||
/// causing lettre to believe the server only supports plaintext connections.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Opportunistic(TlsParameters),
|
||||
/// Begin with a plaintext connection and require `STARTTLS` for security.
|
||||
///
|
||||
/// lettre will upgrade plaintext TCP connections to TLS before transmitting
|
||||
/// any sensitive data. If the server does not support TLS, the connection
|
||||
/// attempt will fail, ensuring no credentials or emails are sent in plaintext.
|
||||
///
|
||||
/// Unlike [`Tls::Opportunistic`], this option is secure against MITM attacks.
|
||||
/// For optimal security and performance, consider using [`Tls::Wrapper`] instead,
|
||||
/// as it requires fewer roundtrips to establish a secure connection.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Required(TlsParameters),
|
||||
/// Establish a connection wrapped in TLS from the start.
|
||||
///
|
||||
/// lettre connects to the server and immediately performs a TLS handshake.
|
||||
/// If the handshake fails, the connection attempt is aborted without
|
||||
/// transmitting any sensitive data.
|
||||
///
|
||||
/// This is the fastest and most secure option for establishing a connection.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
impl Debug for Tls {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
Self::None => f.pad("None"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Opportunistic(_) => f.pad("Opportunistic"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Required(_) => f.pad("Required"),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Self::Wrapper(_) => f.pad("Wrapper"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Source for the base set of root certificates to trust.
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[derive(Clone, Debug, Default)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `CertificateStore` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub enum CertificateStore {
|
||||
/// Use the default for the TLS backend.
|
||||
///
|
||||
/// For native-tls, this will use the system certificate store on Windows, the keychain on
|
||||
/// macOS, and OpenSSL directories on Linux (usually `/etc/ssl`).
|
||||
///
|
||||
/// For rustls, this will use the system certificate verifier if the `rustls-platform-verifier`
|
||||
/// feature is enabled. If the `rustls-native-certs` feature is enabled, system certificate
|
||||
/// store will be used. Otherwise, it will fall back to `webpki-roots`.
|
||||
///
|
||||
/// The boring-tls backend uses the same logic as OpenSSL on all platforms.
|
||||
#[default]
|
||||
Default,
|
||||
/// Use a hardcoded set of Mozilla roots via the `webpki-roots` crate.
|
||||
///
|
||||
/// This option is only available in the rustls backend.
|
||||
#[cfg(all(feature = "rustls", feature = "webpki-roots"))]
|
||||
WebpkiRoots,
|
||||
/// Don't use any system certificates.
|
||||
None,
|
||||
}
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `TlsParameters` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct TlsParameters {
|
||||
pub(in crate::transport::smtp) inner: InnerTlsParameters,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `TlsParametersBuilder` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
cert_store: CertificateStore,
|
||||
root_certs: Vec<Certificate>,
|
||||
identity: Option<Identity>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
min_tls_version: TlsVersion,
|
||||
}
|
||||
|
||||
impl TlsParametersBuilder {
|
||||
/// Creates a new builder for `TlsParameters`
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
cert_store: CertificateStore::Default,
|
||||
root_certs: Vec::new(),
|
||||
identity: None,
|
||||
accept_invalid_hostnames: false,
|
||||
accept_invalid_certs: false,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
min_tls_version: TlsVersion::Tlsv12,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the source for the base set of root certificates to trust.
|
||||
pub fn certificate_store(mut self, cert_store: CertificateStore) -> Self {
|
||||
self.cert_store = cert_store;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a custom root certificate
|
||||
///
|
||||
/// Can be used to safely connect to a server using a self-signed certificate, for example.
|
||||
pub fn add_root_certificate(mut self, cert: Certificate) -> Self {
|
||||
self.root_certs.push(cert);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a client certificate
|
||||
///
|
||||
/// Can be used to configure a client certificate to present to the server.
|
||||
pub fn identify_with(mut self, identity: Identity) -> Self {
|
||||
self.identity = Some(identity);
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether certificates with an invalid hostname are accepted
|
||||
///
|
||||
/// This option is silently disabled when using `rustls-platform-verifier`.
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If hostname verification is disabled *any* valid certificate,
|
||||
/// including those from other sites, are trusted.
|
||||
///
|
||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls which minimum TLS version is allowed
|
||||
///
|
||||
/// Defaults to [`Tlsv12`][TlsVersion::Tlsv12].
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn set_min_tls_version(mut self, min_tls_version: TlsVersion) -> Self {
|
||||
self.min_tls_version = min_tls_version;
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether invalid certificates are accepted
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If certificate verification is disabled, *any* certificate
|
||||
/// is trusted for use, including:
|
||||
///
|
||||
/// * Self signed certificates
|
||||
/// * Certificates from different hostnames
|
||||
/// * Expired certificates
|
||||
///
|
||||
/// This method should only be used as a last resort, as it introduces
|
||||
/// significant vulnerabilities to man-in-the-middle attacks.
|
||||
pub fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
|
||||
self.accept_invalid_certs = accept_invalid_certs;
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls, boring-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||
#[cfg(feature = "rustls")]
|
||||
return self.build_rustls();
|
||||
#[cfg(all(not(feature = "rustls"), feature = "native-tls"))]
|
||||
return self.build_native();
|
||||
#[cfg(all(not(feature = "rustls"), feature = "boring-tls"))]
|
||||
return self.build_boring();
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
let cert_store = match self.cert_store {
|
||||
CertificateStore::Default => super::native_tls::CertificateStore::System,
|
||||
CertificateStore::None => super::native_tls::CertificateStore::None,
|
||||
#[allow(unreachable_patterns)]
|
||||
other => {
|
||||
return Err(error::tls(format!(
|
||||
"{other:?} is not supported in native tls"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let min_tls_version = match self.min_tls_version {
|
||||
TlsVersion::Tlsv10 => super::native_tls::MinTlsVersion::Tlsv10,
|
||||
TlsVersion::Tlsv11 => super::native_tls::MinTlsVersion::Tlsv11,
|
||||
TlsVersion::Tlsv12 => super::native_tls::MinTlsVersion::Tlsv12,
|
||||
TlsVersion::Tlsv13 => {
|
||||
return Err(error::tls(
|
||||
"min tls version Tlsv13 not supported in native tls",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let mut builder = super::TlsParametersBuilder::<super::NativeTls>::new(self.domain)
|
||||
.certificate_store(cert_store)
|
||||
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
|
||||
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
|
||||
.min_tls_version(min_tls_version);
|
||||
for cert in self.root_certs {
|
||||
builder = builder.add_root_certificate(cert.native_tls);
|
||||
}
|
||||
if let Some(identity) = self.identity {
|
||||
builder = builder.identify_with(identity.native_tls);
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::NativeTls::__build_current_tls_parameters)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using boring-tls with the provided configuration
|
||||
///
|
||||
/// Warning: this uses the certificate store passed via `certificate_store`
|
||||
/// instead of the one configured in [`TlsParametersBuilder::certificate_store`].
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn build_boring(self) -> Result<TlsParameters, Error> {
|
||||
let cert_store = match self.cert_store {
|
||||
CertificateStore::Default => super::boring_tls::CertificateStore::System,
|
||||
CertificateStore::None => super::boring_tls::CertificateStore::None,
|
||||
#[allow(unreachable_patterns)]
|
||||
other => {
|
||||
return Err(error::tls(format!(
|
||||
"{other:?} is not supported in native tls"
|
||||
)))
|
||||
}
|
||||
};
|
||||
let min_tls_version = match self.min_tls_version {
|
||||
TlsVersion::Tlsv10 => super::boring_tls::MinTlsVersion::Tlsv10,
|
||||
TlsVersion::Tlsv11 => super::boring_tls::MinTlsVersion::Tlsv11,
|
||||
TlsVersion::Tlsv12 => super::boring_tls::MinTlsVersion::Tlsv12,
|
||||
TlsVersion::Tlsv13 => super::boring_tls::MinTlsVersion::Tlsv13,
|
||||
};
|
||||
|
||||
let mut builder = super::TlsParametersBuilder::<super::BoringTls>::new(self.domain)
|
||||
.certificate_store(cert_store)
|
||||
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
|
||||
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
|
||||
.min_tls_version(min_tls_version);
|
||||
for cert in self.root_certs {
|
||||
builder = builder.add_root_certificate(cert.boring_tls);
|
||||
}
|
||||
if let Some(identity) = self.identity {
|
||||
builder = builder.identify_with(identity.boring_tls);
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::BoringTls::__build_current_tls_parameters)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
let cert_store = match self.cert_store {
|
||||
CertificateStore::Default => super::rustls::CertificateStore::default(),
|
||||
#[cfg(feature = "webpki-roots")]
|
||||
CertificateStore::WebpkiRoots => super::rustls::CertificateStore::WebpkiRoots,
|
||||
CertificateStore::None => super::rustls::CertificateStore::None,
|
||||
};
|
||||
let min_tls_version = match self.min_tls_version {
|
||||
TlsVersion::Tlsv10 => {
|
||||
return Err(error::tls("min tls version Tlsv10 not supported in rustls"))
|
||||
}
|
||||
TlsVersion::Tlsv11 => {
|
||||
return Err(error::tls("min tls version Tlsv11 not supported in rustls"))
|
||||
}
|
||||
TlsVersion::Tlsv12 => super::rustls::MinTlsVersion::Tlsv12,
|
||||
TlsVersion::Tlsv13 => super::rustls::MinTlsVersion::Tlsv13,
|
||||
};
|
||||
|
||||
let mut builder = super::TlsParametersBuilder::<super::Rustls>::new(self.domain)
|
||||
.certificate_store(cert_store)
|
||||
.dangerous_accept_invalid_certs(self.accept_invalid_certs)
|
||||
.dangerous_accept_invalid_hostnames(self.accept_invalid_hostnames)
|
||||
.min_tls_version(min_tls_version);
|
||||
for cert in self.root_certs {
|
||||
for cert in cert.rustls {
|
||||
builder = builder.add_root_certificate(cert);
|
||||
}
|
||||
}
|
||||
if let Some(identity) = self.identity {
|
||||
builder = builder.identify_with(identity.rustls_tls);
|
||||
}
|
||||
|
||||
builder
|
||||
.build()
|
||||
.map(super::Rustls::__build_current_tls_parameters)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(in crate::transport::smtp) enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(super::TlsParameters<super::NativeTls>),
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls(super::TlsParameters<super::Rustls>),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
BoringTls(super::TlsParameters<super::BoringTls>),
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::DefaultTlsBackend>(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` builder
|
||||
pub fn builder(domain: String) -> TlsParametersBuilder {
|
||||
TlsParametersBuilder::new(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn new_native(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::NativeTls>(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub fn new_rustls(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::Rustls>(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using boring
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub fn new_boring(domain: String) -> Result<Self, Error> {
|
||||
Self::new_with::<super::BoringTls>(domain)
|
||||
}
|
||||
|
||||
fn new_with<B: TlsBackend>(domain: String) -> Result<Self, Error> {
|
||||
super::TlsParametersBuilder::<B>::new(domain)
|
||||
.build()
|
||||
.map(B::__build_current_tls_parameters)
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
match self.inner {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(ref inner) => &inner.server_name,
|
||||
#[cfg(feature = "rustls")]
|
||||
InnerTlsParameters::Rustls(ref inner) => inner.server_name.as_ref(),
|
||||
#[cfg(feature = "boring-tls")]
|
||||
InnerTlsParameters::BoringTls(ref inner) => &inner.server_name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Certificate` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Certificate,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: Vec<super::rustls::Certificate>,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate,
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
impl Certificate {
|
||||
/// Create a `Certificate` from a DER encoded certificate
|
||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Certificate::from_der(&der)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate::from_der(&der)?,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: vec![super::rustls::Certificate::from_der(der)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `Certificate` from a PEM encoded certificate
|
||||
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Certificate::from_pem(pem)?,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls: super::rustls::Certificate::from_pem_bundle(pem)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Certificate::from_pem(pem)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// An identity that can be used with [`TlsParametersBuilder::identify_with`]
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(
|
||||
not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")),
|
||||
deprecated(
|
||||
note = "starting from lettre v0.12 `Identity` won't be available when none of the TLS backends are enabled"
|
||||
)
|
||||
)]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub struct Identity {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Identity,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: super::rustls::Identity,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Identity,
|
||||
}
|
||||
|
||||
impl Debug for Identity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity").finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
impl Identity {
|
||||
pub fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: super::native_tls::Identity::from_pem(pem, key)?,
|
||||
#[cfg(feature = "rustls")]
|
||||
rustls_tls: super::rustls::Identity::from_pem(pem, key)?,
|
||||
#[cfg(feature = "boring-tls")]
|
||||
boring_tls: super::boring_tls::Identity::from_pem(pem, key)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
253
src/transport/smtp/client/tls/mod.rs
Normal file
253
src/transport/smtp/client/tls/mod.rs
Normal file
@@ -0,0 +1,253 @@
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
pub(super) mod boring_tls;
|
||||
pub(super) mod current;
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub(super) mod native_tls;
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
pub(super) mod rustls;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(private_bounds)]
|
||||
pub(in crate::transport::smtp) struct TlsParameters<B: TlsBackend> {
|
||||
pub(in crate::transport::smtp) server_name: B::ServerName,
|
||||
pub(in crate::transport::smtp) connector: B::Connector,
|
||||
pub(in crate::transport::smtp) extra_info: B::ExtraInfo,
|
||||
}
|
||||
|
||||
impl<B: TlsBackend> Clone for TlsParameters<B> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
server_name: self.server_name.clone(),
|
||||
connector: self.connector.clone(),
|
||||
extra_info: self.extra_info.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TlsParametersBuilder<B: TlsBackend> {
|
||||
server_name: String,
|
||||
cert_store: B::CertificateStore,
|
||||
root_certs: Vec<B::Certificate>,
|
||||
identity: Option<B::Identity>,
|
||||
accept_invalid_certs: bool,
|
||||
accept_invalid_hostnames: bool,
|
||||
min_tls_version: B::MinTlsVersion,
|
||||
}
|
||||
|
||||
impl<B: TlsBackend> TlsParametersBuilder<B> {
|
||||
fn new(server_name: String) -> Self {
|
||||
Self {
|
||||
server_name,
|
||||
cert_store: Default::default(),
|
||||
root_certs: Vec::new(),
|
||||
identity: None,
|
||||
accept_invalid_certs: false,
|
||||
accept_invalid_hostnames: false,
|
||||
min_tls_version: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn certificate_store(mut self, cert_store: B::CertificateStore) -> Self {
|
||||
self.cert_store = cert_store;
|
||||
self
|
||||
}
|
||||
|
||||
fn add_root_certificate(mut self, cert: B::Certificate) -> Self {
|
||||
self.root_certs.push(cert);
|
||||
self
|
||||
}
|
||||
|
||||
fn identify_with(mut self, identity: B::Identity) -> Self {
|
||||
self.identity = Some(identity);
|
||||
self
|
||||
}
|
||||
|
||||
fn min_tls_version(mut self, min_tls_version: B::MinTlsVersion) -> Self {
|
||||
self.min_tls_version = min_tls_version;
|
||||
self
|
||||
}
|
||||
|
||||
fn dangerous_accept_invalid_certs(mut self, accept_invalid_certs: bool) -> Self {
|
||||
self.accept_invalid_certs = accept_invalid_certs;
|
||||
self
|
||||
}
|
||||
|
||||
fn dangerous_accept_invalid_hostnames(mut self, accept_invalid_hostnames: bool) -> Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> Result<TlsParameters<B>, Error> {
|
||||
B::__build_connector(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(private_bounds)]
|
||||
trait TlsBackend: private::SealedTlsBackend {
|
||||
type CertificateStore: Default;
|
||||
type Certificate;
|
||||
type Identity;
|
||||
type MinTlsVersion: Default;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn __build_connector(builder: TlsParametersBuilder<Self>)
|
||||
-> Result<TlsParameters<Self>, Error>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters;
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
type DefaultTlsBackend = NativeTls;
|
||||
|
||||
#[cfg(all(feature = "rustls", not(feature = "native-tls")))]
|
||||
type DefaultTlsBackend = Rustls;
|
||||
|
||||
#[cfg(all(
|
||||
feature = "boring-tls",
|
||||
not(feature = "native-tls"),
|
||||
not(feature = "rustls")
|
||||
))]
|
||||
type DefaultTlsBackend = BoringTls;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(in crate::transport::smtp) struct NativeTls;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
impl TlsBackend for NativeTls {
|
||||
type CertificateStore = self::native_tls::CertificateStore;
|
||||
type Certificate = self::native_tls::Certificate;
|
||||
type Identity = self::native_tls::Identity;
|
||||
type MinTlsVersion = self::native_tls::MinTlsVersion;
|
||||
|
||||
fn __build_connector(
|
||||
builder: TlsParametersBuilder<Self>,
|
||||
) -> Result<TlsParameters<Self>, Error> {
|
||||
self::native_tls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
|
||||
server_name,
|
||||
connector,
|
||||
extra_info: (),
|
||||
})
|
||||
}
|
||||
|
||||
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
|
||||
self::current::TlsParameters {
|
||||
inner: self::current::InnerTlsParameters::NativeTls(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(in crate::transport::smtp) struct Rustls;
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
||||
impl TlsBackend for Rustls {
|
||||
type CertificateStore = self::rustls::CertificateStore;
|
||||
type Certificate = self::rustls::Certificate;
|
||||
type Identity = self::rustls::Identity;
|
||||
type MinTlsVersion = self::rustls::MinTlsVersion;
|
||||
|
||||
fn __build_connector(
|
||||
builder: TlsParametersBuilder<Self>,
|
||||
) -> Result<TlsParameters<Self>, Error> {
|
||||
self::rustls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
|
||||
server_name,
|
||||
connector,
|
||||
extra_info: (),
|
||||
})
|
||||
}
|
||||
|
||||
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
|
||||
self::current::TlsParameters {
|
||||
inner: self::current::InnerTlsParameters::Rustls(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(in crate::transport::smtp) struct BoringTls;
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
|
||||
impl TlsBackend for BoringTls {
|
||||
type CertificateStore = self::boring_tls::CertificateStore;
|
||||
type Certificate = self::boring_tls::Certificate;
|
||||
type Identity = self::boring_tls::Identity;
|
||||
type MinTlsVersion = self::boring_tls::MinTlsVersion;
|
||||
|
||||
fn __build_connector(
|
||||
builder: TlsParametersBuilder<Self>,
|
||||
) -> Result<TlsParameters<Self>, Error> {
|
||||
let accept_invalid_hostnames = builder.accept_invalid_hostnames;
|
||||
self::boring_tls::build_connector(builder).map(|(server_name, connector)| TlsParameters {
|
||||
server_name,
|
||||
connector,
|
||||
extra_info: BoringTlsExtraInfo {
|
||||
accept_invalid_hostnames,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn __build_current_tls_parameters(inner: TlsParameters<Self>) -> self::current::TlsParameters {
|
||||
self::current::TlsParameters {
|
||||
inner: self::current::InnerTlsParameters::BoringTls(inner),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(in crate::transport::smtp) struct BoringTlsExtraInfo {
|
||||
pub(super) accept_invalid_hostnames: bool,
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub(in crate::transport::smtp) trait SealedTlsBackend:
|
||||
Sized
|
||||
{
|
||||
type ServerName: Clone + AsRef<str>;
|
||||
type Connector: Clone;
|
||||
type ExtraInfo: Clone;
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
impl SealedTlsBackend for super::NativeTls {
|
||||
type ServerName = Box<str>;
|
||||
type Connector = native_tls::TlsConnector;
|
||||
type ExtraInfo = ();
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
impl SealedTlsBackend for super::Rustls {
|
||||
type ServerName = super::rustls::ServerName;
|
||||
type Connector = std::sync::Arc<rustls::client::ClientConfig>;
|
||||
type ExtraInfo = ();
|
||||
}
|
||||
|
||||
#[cfg(feature = "boring-tls")]
|
||||
impl SealedTlsBackend for super::BoringTls {
|
||||
type ServerName = Box<str>;
|
||||
type Connector = boring::ssl::SslConnector;
|
||||
type ExtraInfo = super::BoringTlsExtraInfo;
|
||||
}
|
||||
}
|
||||
95
src/transport/smtp/client/tls/native_tls.rs
Normal file
95
src/transport/smtp/client/tls/native_tls.rs
Normal file
@@ -0,0 +1,95 @@
|
||||
use std::fmt::{self, Debug};
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
|
||||
pub(super) fn build_connector(
|
||||
builder: super::TlsParametersBuilder<super::NativeTls>,
|
||||
) -> Result<(Box<str>, TlsConnector), Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
|
||||
match builder.cert_store {
|
||||
CertificateStore::System => {}
|
||||
CertificateStore::None => {
|
||||
tls_builder.disable_built_in_roots(true);
|
||||
}
|
||||
}
|
||||
for cert in builder.root_certs {
|
||||
tls_builder.add_root_certificate(cert.0);
|
||||
}
|
||||
tls_builder.danger_accept_invalid_hostnames(builder.accept_invalid_hostnames);
|
||||
tls_builder.danger_accept_invalid_certs(builder.accept_invalid_certs);
|
||||
|
||||
let min_tls_version = match builder.min_tls_version {
|
||||
MinTlsVersion::Tlsv10 => native_tls::Protocol::Tlsv10,
|
||||
MinTlsVersion::Tlsv11 => native_tls::Protocol::Tlsv11,
|
||||
MinTlsVersion::Tlsv12 => native_tls::Protocol::Tlsv12,
|
||||
};
|
||||
|
||||
tls_builder.min_protocol_version(Some(min_tls_version));
|
||||
if let Some(identity) = builder.identity {
|
||||
tls_builder.identity(identity.0);
|
||||
}
|
||||
|
||||
let connector = tls_builder.build().map_err(error::tls)?;
|
||||
Ok((builder.server_name.into_boxed_str(), connector))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum CertificateStore {
|
||||
#[default]
|
||||
System,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Certificate(pub(super) native_tls::Certificate);
|
||||
|
||||
impl Certificate {
|
||||
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self(
|
||||
native_tls::Certificate::from_pem(pem).map_err(error::tls)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn from_der(der: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self(
|
||||
native_tls::Certificate::from_der(der).map_err(error::tls)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Identity(pub(super) native_tls::Identity);
|
||||
|
||||
impl Identity {
|
||||
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
|
||||
Ok(Self(
|
||||
native_tls::Identity::from_pkcs8(pem, key).map_err(error::tls)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Identity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum MinTlsVersion {
|
||||
Tlsv10,
|
||||
Tlsv11,
|
||||
#[default]
|
||||
Tlsv12,
|
||||
}
|
||||
329
src/transport/smtp/client/tls/rustls.rs
Normal file
329
src/transport/smtp/client/tls/rustls.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use rustls::{
|
||||
client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier},
|
||||
crypto::{verify_tls12_signature, verify_tls13_signature, CryptoProvider},
|
||||
pki_types::{self, UnixTime},
|
||||
server::ParsedCertificate,
|
||||
ClientConfig, DigitallySignedStruct, RootCertStore, SignatureScheme,
|
||||
};
|
||||
|
||||
use crate::transport::smtp::error::{self, Error};
|
||||
|
||||
pub(super) fn build_connector(
|
||||
builder: super::TlsParametersBuilder<super::Rustls>,
|
||||
) -> Result<(ServerName, Arc<ClientConfig>), Error> {
|
||||
let just_version3 = &[&rustls::version::TLS13];
|
||||
let supported_versions = match builder.min_tls_version {
|
||||
MinTlsVersion::Tlsv12 => rustls::ALL_VERSIONS,
|
||||
MinTlsVersion::Tlsv13 => just_version3,
|
||||
};
|
||||
|
||||
let crypto_provider = crate::rustls_crypto::crypto_provider();
|
||||
let tls = ClientConfig::builder_with_provider(Arc::clone(&crypto_provider))
|
||||
.with_protocol_versions(supported_versions)
|
||||
.map_err(error::tls)?;
|
||||
|
||||
// Build TLS config
|
||||
let mut root_cert_store = RootCertStore::empty();
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
let mut extra_roots = Vec::new();
|
||||
|
||||
match builder.cert_store {
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
CertificateStore::PlatformVerifier => {
|
||||
extra_roots = builder
|
||||
.root_certs
|
||||
.iter()
|
||||
.map(|cert| cert.0.clone())
|
||||
.collect();
|
||||
}
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
CertificateStore::NativeCerts => {
|
||||
let rustls_native_certs::CertificateResult { certs, errors, .. } =
|
||||
rustls_native_certs::load_native_certs();
|
||||
let errors_len = errors.len();
|
||||
|
||||
let (added, ignored) = root_cert_store.add_parsable_certificates(certs);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!(
|
||||
"loaded platform certs with {errors_len} failing to load, {added} valid and {ignored} ignored (invalid) certs"
|
||||
);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = (errors_len, added, ignored);
|
||||
}
|
||||
#[cfg(feature = "webpki-roots")]
|
||||
CertificateStore::WebpkiRoots => {
|
||||
root_cert_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
}
|
||||
CertificateStore::None => {}
|
||||
}
|
||||
for cert in builder.root_certs {
|
||||
root_cert_store.add(cert.0).map_err(error::tls)?;
|
||||
}
|
||||
|
||||
let tls = match (
|
||||
builder.cert_store,
|
||||
builder.accept_invalid_certs,
|
||||
builder.accept_invalid_hostnames,
|
||||
) {
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
(CertificateStore::PlatformVerifier, false, _) => {
|
||||
tls.dangerous().with_custom_certificate_verifier(Arc::new(
|
||||
rustls_platform_verifier::Verifier::new_with_extra_roots(
|
||||
extra_roots,
|
||||
crypto_provider,
|
||||
)
|
||||
.map_err(error::tls)?,
|
||||
))
|
||||
}
|
||||
(_, true, _) | (_, _, true) => {
|
||||
let verifier = InvalidCertsVerifier {
|
||||
ignore_invalid_hostnames: builder.accept_invalid_hostnames,
|
||||
ignore_invalid_certs: builder.accept_invalid_certs,
|
||||
roots: root_cert_store,
|
||||
crypto_provider,
|
||||
};
|
||||
tls.dangerous()
|
||||
.with_custom_certificate_verifier(Arc::new(verifier))
|
||||
}
|
||||
_ => tls.with_root_certificates(root_cert_store),
|
||||
};
|
||||
|
||||
let tls = if let Some(identity) = builder.identity {
|
||||
tls.with_client_auth_cert(identity.chain, identity.key)
|
||||
.map_err(error::tls)?
|
||||
} else {
|
||||
tls.with_no_client_auth()
|
||||
};
|
||||
let server_name = ServerName::try_from(builder.server_name)?;
|
||||
Ok((server_name, Arc::new(tls)))
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(in crate::transport::smtp) struct ServerName {
|
||||
val: pki_types::ServerName<'static>,
|
||||
str_val: Box<str>,
|
||||
}
|
||||
|
||||
impl ServerName {
|
||||
#[allow(dead_code)]
|
||||
pub(in crate::transport::smtp) fn inner(self) -> pki_types::ServerName<'static> {
|
||||
self.val
|
||||
}
|
||||
|
||||
pub(in crate::transport::smtp) fn inner_ref(&self) -> &pki_types::ServerName<'static> {
|
||||
&self.val
|
||||
}
|
||||
|
||||
fn try_from(value: String) -> Result<Self, crate::transport::smtp::Error> {
|
||||
let val: pki_types::ServerName<'_> = value
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(crate::transport::smtp::error::tls)?;
|
||||
Ok(Self {
|
||||
val: val.to_owned(),
|
||||
str_val: value.into_boxed_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for ServerName {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.str_val
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(dead_code, missing_copy_implementations)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum CertificateStore {
|
||||
#[cfg(feature = "rustls-platform-verifier")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-platform-verifier")))]
|
||||
#[cfg_attr(feature = "rustls-platform-verifier", default)]
|
||||
PlatformVerifier,
|
||||
#[cfg(feature = "rustls-native-certs")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-native-certs")))]
|
||||
#[cfg_attr(
|
||||
all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
feature = "rustls-native-certs",
|
||||
),
|
||||
default
|
||||
)]
|
||||
NativeCerts,
|
||||
#[cfg(feature = "webpki-roots")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "webpki-roots")))]
|
||||
#[cfg_attr(
|
||||
all(
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs"),
|
||||
feature = "webpki-roots",
|
||||
),
|
||||
default
|
||||
)]
|
||||
WebpkiRoots,
|
||||
#[cfg_attr(
|
||||
all(
|
||||
not(feature = "webpki-roots"),
|
||||
not(feature = "rustls-platform-verifier"),
|
||||
not(feature = "rustls-native-certs")
|
||||
),
|
||||
default
|
||||
)]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Certificate(pub(super) pki_types::CertificateDer<'static>);
|
||||
|
||||
impl Certificate {
|
||||
#[allow(dead_code)]
|
||||
pub(super) fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
use rustls::pki_types::pem::PemObject as _;
|
||||
|
||||
Ok(Self(
|
||||
pki_types::CertificateDer::from_pem_slice(pem)
|
||||
.map_err(|_| error::tls("invalid certificate"))?,
|
||||
))
|
||||
}
|
||||
|
||||
pub(super) fn from_pem_bundle(pem: &[u8]) -> Result<Vec<Self>, Error> {
|
||||
use rustls::pki_types::pem::PemObject as _;
|
||||
|
||||
pki_types::CertificateDer::pem_slice_iter(pem)
|
||||
.map(|cert| Ok(Self(cert?)))
|
||||
.collect::<Result<Vec<_>, pki_types::pem::Error>>()
|
||||
.map_err(|_| error::tls("invalid certificate"))
|
||||
}
|
||||
|
||||
pub(super) fn from_der(der: Vec<u8>) -> Self {
|
||||
Self(der.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Certificate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Certificate").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct Identity {
|
||||
pub(super) chain: Vec<pki_types::CertificateDer<'static>>,
|
||||
pub(super) key: pki_types::PrivateKeyDer<'static>,
|
||||
}
|
||||
|
||||
impl Identity {
|
||||
pub(super) fn from_pem(pem: &[u8], key: &[u8]) -> Result<Self, Error> {
|
||||
use rustls::pki_types::pem::PemObject as _;
|
||||
|
||||
let key = match pki_types::PrivateKeyDer::from_pem_slice(key) {
|
||||
Ok(key) => key,
|
||||
Err(pki_types::pem::Error::NoItemsFound) => {
|
||||
return Err(error::tls("no private key found"))
|
||||
}
|
||||
Err(err) => return Err(error::tls(err)),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
chain: vec![pem.to_owned().into()],
|
||||
key,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Identity {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
chain: self.chain.clone(),
|
||||
key: self.key.clone_key(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Identity {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Identity").finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[non_exhaustive]
|
||||
pub(super) enum MinTlsVersion {
|
||||
#[default]
|
||||
Tlsv12,
|
||||
Tlsv13,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct InvalidCertsVerifier {
|
||||
ignore_invalid_hostnames: bool,
|
||||
ignore_invalid_certs: bool,
|
||||
roots: RootCertStore,
|
||||
crypto_provider: Arc<CryptoProvider>,
|
||||
}
|
||||
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
end_entity: &pki_types::CertificateDer<'_>,
|
||||
intermediates: &[pki_types::CertificateDer<'_>],
|
||||
server_name: &pki_types::ServerName<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
let cert = ParsedCertificate::try_from(end_entity)?;
|
||||
|
||||
if !self.ignore_invalid_certs {
|
||||
rustls::client::verify_server_cert_signed_by_trust_anchor(
|
||||
&cert,
|
||||
&self.roots,
|
||||
intermediates,
|
||||
now,
|
||||
self.crypto_provider.signature_verification_algorithms.all,
|
||||
)?;
|
||||
}
|
||||
|
||||
if !self.ignore_invalid_hostnames {
|
||||
rustls::client::verify_server_name(&cert, server_name)?;
|
||||
}
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &pki_types::CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls12_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&self.crypto_provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
message: &[u8],
|
||||
cert: &pki_types::CertificateDer<'_>,
|
||||
dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
verify_tls13_signature(
|
||||
message,
|
||||
cert,
|
||||
dss,
|
||||
&self.crypto_provider.signature_verification_algorithms,
|
||||
)
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
|
||||
self.crypto_provider
|
||||
.signature_verification_algorithms
|
||||
.supported_schemes()
|
||||
}
|
||||
}
|
||||
371
src/transport/smtp/commands.rs
Normal file
371
src/transport/smtp/commands.rs
Normal file
@@ -0,0 +1,371 @@
|
||||
//! SMTP commands
|
||||
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use crate::{
|
||||
address::Address,
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::{self, Error},
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Ehlo {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for Ehlo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ehlo {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> Ehlo {
|
||||
Ehlo { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Starttls;
|
||||
|
||||
impl Display for Starttls {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Mail {
|
||||
sender: Option<Address>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for Mail {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map_or("", |s| s.as_ref())
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {parameter}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Mail {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
|
||||
Mail { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rcpt {
|
||||
recipient: Address,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for Rcpt {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {parameter}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Rcpt {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
|
||||
Rcpt {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Data;
|
||||
|
||||
impl Display for Data {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Quit;
|
||||
|
||||
impl Display for Quit {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Noop;
|
||||
|
||||
impl Display for Noop {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Help {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if let Some(argument) = &self.argument {
|
||||
write!(f, " {argument}")?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Help {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> Help {
|
||||
Help { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Vrfy {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for Vrfy {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl Vrfy {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> Vrfy {
|
||||
Vrfy { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Expn {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for Expn {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl Expn {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> Expn {
|
||||
Expn { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rset;
|
||||
|
||||
impl Display for Rset {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Auth {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Auth {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let encoded_response = self.response.as_ref().map(crate::base64::encode);
|
||||
|
||||
if self.mechanism.supports_initial_response() {
|
||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||
} else {
|
||||
match encoded_response {
|
||||
Some(response) => f.write_str(&response)?,
|
||||
None => write!(f, "AUTH {}", self.mechanism)?,
|
||||
}
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<Auth, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an AUTH command from a response that needs to be a
|
||||
/// valid challenge (with 334 response code)
|
||||
pub fn new_from_response(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(error::response("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or_else(|| error::response("Could not read auth challenge"))?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_base64 = crate::base64::decode(encoded_challenge).map_err(error::response)?;
|
||||
let decoded_challenge = String::from_utf8(decoded_base64).map_err(error::response)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::transport::smtp::extension::MailBodyParameter;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_owned());
|
||||
let email = Address::from_str("test@example.com").unwrap();
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_owned(),
|
||||
value: Some("value".to_owned()),
|
||||
};
|
||||
assert_eq!(format!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Mail::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mail::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
MailParameter::Body(MailBodyParameter::EightBitMime),
|
||||
mail_parameter,
|
||||
],
|
||||
)
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Rcpt::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{Quit}"), "QUIT\r\n");
|
||||
assert_eq!(format!("{Data}"), "DATA\r\n");
|
||||
assert_eq!(format!("{Noop}"), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Help::new(Some("test".to_owned()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Vrfy::new("test".to_owned())), "VRFY test\r\n");
|
||||
assert_eq!(format!("{}", Expn::new("test".to_owned())), "EXPN test\r\n");
|
||||
assert_eq!(format!("{Rset}"), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_owned(), "password".to_owned());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Auth::new(Mechanism::Login, credentials, None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
134
src/transport/smtp/connection_url.rs
Normal file
134
src/transport/smtp/connection_url.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use url::Url;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::client::{Tls, TlsParameters};
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use super::AsyncSmtpTransportBuilder;
|
||||
use super::{
|
||||
authentication::Credentials, error, extension::ClientId, Error, SmtpTransportBuilder,
|
||||
SMTP_PORT, SUBMISSIONS_PORT, SUBMISSION_PORT,
|
||||
};
|
||||
|
||||
pub(crate) trait TransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self;
|
||||
fn port(self, port: u16) -> Self;
|
||||
fn credentials(self, credentials: Credentials) -> Self;
|
||||
fn hello_name(self, name: ClientId) -> Self;
|
||||
}
|
||||
|
||||
impl TransportBuilder for SmtpTransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
|
||||
fn port(self, port: u16) -> Self {
|
||||
self.port(port)
|
||||
}
|
||||
|
||||
fn credentials(self, credentials: Credentials) -> Self {
|
||||
self.credentials(credentials)
|
||||
}
|
||||
|
||||
fn hello_name(self, name: ClientId) -> Self {
|
||||
self.hello_name(name)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
impl TransportBuilder for AsyncSmtpTransportBuilder {
|
||||
fn new<T: Into<String>>(server: T) -> Self {
|
||||
Self::new(server)
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
fn tls(self, tls: super::Tls) -> Self {
|
||||
self.tls(tls)
|
||||
}
|
||||
|
||||
fn port(self, port: u16) -> Self {
|
||||
self.port(port)
|
||||
}
|
||||
|
||||
fn credentials(self, credentials: Credentials) -> Self {
|
||||
self.credentials(credentials)
|
||||
}
|
||||
|
||||
fn hello_name(self, name: ClientId) -> Self {
|
||||
self.hello_name(name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `SmtpTransportBuilder` or `AsyncSmtpTransportBuilder` from a connection URL
|
||||
pub(crate) fn from_connection_url<B: TransportBuilder>(connection_url: &str) -> Result<B, Error> {
|
||||
let connection_url = Url::parse(connection_url).map_err(error::connection)?;
|
||||
let tls: Option<String> = connection_url
|
||||
.query_pairs()
|
||||
.find(|(k, _)| k == "tls")
|
||||
.map(|(_, v)| v.to_string());
|
||||
|
||||
let host = connection_url
|
||||
.host_str()
|
||||
.ok_or_else(|| error::connection("smtp host undefined"))?;
|
||||
|
||||
let mut builder = B::new(host);
|
||||
|
||||
match (connection_url.scheme(), tls.as_deref()) {
|
||||
("smtp", None) => {
|
||||
builder = builder.port(connection_url.port().unwrap_or(SMTP_PORT));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtp", Some("required")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Required(TlsParameters::new(host.into())?));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtp", Some("opportunistic")) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSION_PORT))
|
||||
.tls(Tls::Opportunistic(TlsParameters::new(host.into())?));
|
||||
}
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
("smtps", _) => {
|
||||
builder = builder
|
||||
.port(connection_url.port().unwrap_or(SUBMISSIONS_PORT))
|
||||
.tls(Tls::Wrapper(TlsParameters::new(host.into())?));
|
||||
}
|
||||
(scheme, tls) => {
|
||||
return Err(error::connection(format!(
|
||||
"Unknown scheme '{scheme}' or tls parameter '{tls:?}', note that a transport with TLS requires one of the TLS features"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
// use the path segment of the URL as name in the name in the HELO / EHLO command
|
||||
if connection_url.path().len() > 1 {
|
||||
let name = connection_url.path().trim_matches('/').to_owned();
|
||||
builder = builder.hello_name(ClientId::Domain(name));
|
||||
}
|
||||
|
||||
if let Some(password) = connection_url.password() {
|
||||
let percent_decode = |s: &str| {
|
||||
percent_encoding::percent_decode_str(s)
|
||||
.decode_utf8()
|
||||
.map(Cow::into_owned)
|
||||
.map_err(error::connection)
|
||||
};
|
||||
let credentials = Credentials::new(
|
||||
percent_decode(connection_url.username())?,
|
||||
percent_decode(password)?,
|
||||
);
|
||||
builder = builder.credentials(credentials);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
}
|
||||
203
src/transport/smtp/error.rs
Normal file
203
src/transport/smtp/error.rs
Normal file
@@ -0,0 +1,203 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
use crate::{
|
||||
transport::smtp::response::{Code, Severity},
|
||||
BoxError,
|
||||
};
|
||||
|
||||
// Inspired by https://github.com/seanmonstar/reqwest/blob/a8566383168c0ef06c21f38cbc9213af6ff6db31/src/error.rs
|
||||
|
||||
/// The Errors that may occur when sending an email over SMTP
|
||||
pub struct Error {
|
||||
inner: Box<Inner>,
|
||||
}
|
||||
|
||||
struct Inner {
|
||||
kind: Kind,
|
||||
source: Option<BoxError>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn new<E>(kind: Kind, source: Option<E>) -> Error
|
||||
where
|
||||
E: Into<BoxError>,
|
||||
{
|
||||
Error {
|
||||
inner: Box::new(Inner {
|
||||
kind,
|
||||
source: source.map(Into::into),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the error is from response
|
||||
pub fn is_response(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Response)
|
||||
}
|
||||
|
||||
/// Returns true if the error is from client
|
||||
pub fn is_client(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Client)
|
||||
}
|
||||
|
||||
/// Returns true if the error is a transient SMTP error
|
||||
pub fn is_transient(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Transient(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is a permanent SMTP error
|
||||
pub fn is_permanent(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Permanent(_))
|
||||
}
|
||||
|
||||
/// Returns true if the error is caused by a timeout
|
||||
pub fn is_timeout(&self) -> bool {
|
||||
let mut source = self.source();
|
||||
|
||||
while let Some(err) = source {
|
||||
if let Some(io_err) = err.downcast_ref::<std::io::Error>() {
|
||||
return io_err.kind() == std::io::ErrorKind::TimedOut;
|
||||
}
|
||||
|
||||
source = err.source();
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if the error is from TLS
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn is_tls(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::Tls)
|
||||
}
|
||||
|
||||
/// Returns true if the error is because the transport was shut down
|
||||
pub fn is_transport_shutdown(&self) -> bool {
|
||||
matches!(self.inner.kind, Kind::TransportShutdown)
|
||||
}
|
||||
|
||||
/// Returns the status code, if the error was generated from a response.
|
||||
pub fn status(&self) -> Option<Code> {
|
||||
match self.inner.kind {
|
||||
Kind::Transient(code) | Kind::Permanent(code) => Some(code),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Kind {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Code),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Code),
|
||||
/// Error parsing a response
|
||||
Response,
|
||||
/// Internal client error
|
||||
Client,
|
||||
/// Connection error
|
||||
Connection,
|
||||
/// Underlying network i/o error
|
||||
Network,
|
||||
/// TLS error
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Tls,
|
||||
/// Transport shutdown error
|
||||
TransportShutdown,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut builder = f.debug_struct("lettre::transport::smtp::Error");
|
||||
|
||||
builder.field("kind", &self.inner.kind);
|
||||
|
||||
if let Some(source) = &self.inner.source {
|
||||
builder.field("source", source);
|
||||
}
|
||||
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match &self.inner.kind {
|
||||
Kind::Response => f.write_str("response error")?,
|
||||
Kind::Client => f.write_str("internal client error")?,
|
||||
Kind::Network => f.write_str("network error")?,
|
||||
Kind::Connection => f.write_str("Connection error")?,
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Kind::Tls => f.write_str("tls error")?,
|
||||
Kind::TransportShutdown => f.write_str("transport has been shut down")?,
|
||||
Kind::Transient(code) => {
|
||||
write!(f, "transient error ({code})")?;
|
||||
}
|
||||
Kind::Permanent(code) => {
|
||||
write!(f, "permanent error ({code})")?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(e) = &self.inner.source {
|
||||
write!(f, ": {e}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
self.inner.source.as_ref().map(|e| {
|
||||
let r: &(dyn std::error::Error + 'static) = &**e;
|
||||
r
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn code(c: Code, s: Option<String>) -> Error {
|
||||
match c.severity {
|
||||
Severity::TransientNegativeCompletion => Error::new(Kind::Transient(c), s),
|
||||
Severity::PermanentNegativeCompletion => Error::new(Kind::Permanent(c), s),
|
||||
_ => client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn response<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Response, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn client<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Client, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn network<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Network, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn connection<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Connection, Some(e))
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
pub(crate) fn tls<E: Into<BoxError>>(e: E) -> Error {
|
||||
Error::new(Kind::Tls, Some(e))
|
||||
}
|
||||
|
||||
pub(crate) fn transport_shutdown() -> Error {
|
||||
Error::new::<BoxError>(Kind::TransportShutdown, None)
|
||||
}
|
||||
@@ -1,83 +1,147 @@
|
||||
//! ESMTP features
|
||||
|
||||
use std::result::Result;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::collections::HashSet;
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
};
|
||||
|
||||
use transport::error::Error;
|
||||
use transport::smtp::response::Response;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
error::{self, Error},
|
||||
response::Response,
|
||||
util::XText,
|
||||
};
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientId {
|
||||
/// A fully-qualified domain name
|
||||
Domain(String),
|
||||
/// An IPv4 address
|
||||
Ipv4(Ipv4Addr),
|
||||
/// An IPv6 address
|
||||
Ipv6(Ipv6Addr),
|
||||
}
|
||||
|
||||
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
impl Default for ClientId {
|
||||
fn default() -> Self {
|
||||
// https://tools.ietf.org/html/rfc5321#section-4.1.4
|
||||
//
|
||||
// The SMTP client MUST, if possible, ensure that the domain parameter
|
||||
// to the EHLO command is a primary host name as specified for this
|
||||
// command in Section 2.3.5. If this is not possible (e.g., when the
|
||||
// client's address is dynamically assigned and the client does not have
|
||||
// an obvious name), an address literal SHOULD be substituted for the
|
||||
// domain name.
|
||||
#[cfg(feature = "hostname")]
|
||||
{
|
||||
hostname::get()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().map(Self::Domain).ok())
|
||||
.unwrap_or(LOCALHOST_CLIENT)
|
||||
}
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
LOCALHOST_CLIENT
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Domain(value) => f.write_str(value),
|
||||
Self::Ipv4(value) => write!(f, "[{value}]"),
|
||||
Self::Ipv6(value) => write!(f, "[IPv6:{value}]"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
|
||||
/// Creates a new `ClientId` from a fully qualified domain name
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self::Domain(domain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
/// RFC 6152: https://tools.ietf.org/html/rfc6152
|
||||
/// Defined in [RFC 6152](https://tools.ietf.org/html/rfc6152)
|
||||
EightBitMime,
|
||||
/// SMTPUTF8 keyword
|
||||
///
|
||||
/// RFC 6531: https://tools.ietf.org/html/rfc6531
|
||||
/// Defined in [RFC 6531](https://tools.ietf.org/html/rfc6531)
|
||||
SmtpUtfEight,
|
||||
/// STARTTLS keyword
|
||||
///
|
||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||
/// Defined in [RFC 2487](https://tools.ietf.org/html/rfc2487)
|
||||
StartTls,
|
||||
/// AUTH mechanism
|
||||
Authentication(Mechanism),
|
||||
}
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
|
||||
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
|
||||
Extension::StartTls => write!(f, "{}", "STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "{} {}", "AUTH", mechanism),
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
Extension::StartTls => f.write_str("STARTTLS"),
|
||||
Extension::Authentication(mechanism) => write!(f, "AUTH {mechanism}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone,Debug,Eq,PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ServerInfo {
|
||||
/// Server name
|
||||
///
|
||||
/// The name given in the server banner
|
||||
pub name: String,
|
||||
name: String,
|
||||
/// ESMTP features supported by the server
|
||||
///
|
||||
/// It contains the features supported by the server and known by the `Extension` module.
|
||||
pub features: HashSet<Extension>,
|
||||
features: HashSet<Extension>,
|
||||
}
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
match self.features.is_empty() {
|
||||
true => "no supported features".to_string(),
|
||||
false => format!("{:?}", self.features),
|
||||
})
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let features = if self.features.is_empty() {
|
||||
"no supported features".to_owned()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
};
|
||||
write!(f, "{} with {}", self.name, features)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerInfo {
|
||||
/// Parses a response to create a `ServerInfo`
|
||||
/// Parses a EHLO response to create a `ServerInfo`
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(Error::ResponseParsingError("Could not read server name")),
|
||||
let Some(name) = response.first_word() else {
|
||||
return Err(error::response("Could not read server name"));
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
|
||||
for line in response.message() {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let splitted: Vec<&str> = line.split_whitespace().collect();
|
||||
let _ = match splitted[0] {
|
||||
let mut split = line.split_whitespace();
|
||||
match split.next().unwrap() {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
@@ -88,53 +152,166 @@ impl ServerInfo {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for &mechanism in &splitted[1..] {
|
||||
for mechanism in split {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
}
|
||||
"CRAM-MD5" => {
|
||||
features.insert(Extension::Authentication(Mechanism::CramMd5));
|
||||
"LOGIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Login));
|
||||
}
|
||||
"XOAUTH2" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Xoauth2));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name,
|
||||
features: features,
|
||||
name: name.to_owned(),
|
||||
features,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_feature(&self, keyword: &Extension) -> bool {
|
||||
self.features.contains(keyword)
|
||||
pub fn supports_feature(&self, keyword: Extension) -> bool {
|
||||
self.features.contains(&keyword)
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features.contains(&Extension::Authentication(mechanism))
|
||||
self.features
|
||||
.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
|
||||
/// Gets a compatible mechanism from a list
|
||||
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||
for mechanism in mechanisms {
|
||||
if self.supports_auth_mechanism(*mechanism) {
|
||||
return Some(*mechanism);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// The name given in the server banner
|
||||
pub fn name(&self) -> &str {
|
||||
self.name.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A `MAIL FROM` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailParameter {
|
||||
/// `BODY` parameter
|
||||
Body(MailBodyParameter),
|
||||
/// `SIZE` parameter
|
||||
Size(usize),
|
||||
/// `SMTPUTF8` parameter
|
||||
SmtpUtfEight,
|
||||
/// Custom parameter
|
||||
Other {
|
||||
/// Parameter keyword
|
||||
keyword: String,
|
||||
/// Parameter value
|
||||
value: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for MailParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
MailParameter::Body(value) => write!(f, "BODY={value}"),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={size}"),
|
||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
MailParameter::Other {
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
MailParameter::Other {
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Values for the `BODY` parameter to `MAIL FROM`
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailBodyParameter {
|
||||
/// `7BIT`
|
||||
SevenBit,
|
||||
/// `8BITMIME`
|
||||
EightBitMime,
|
||||
}
|
||||
|
||||
impl Display for MailBodyParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailBodyParameter::SevenBit => f.write_str("7BIT"),
|
||||
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `RCPT TO` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RcptParameter {
|
||||
/// Custom parameter
|
||||
Other {
|
||||
/// Parameter keyword
|
||||
keyword: String,
|
||||
/// Parameter value
|
||||
value: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match &self {
|
||||
RcptParameter::Other {
|
||||
keyword,
|
||||
value: Some(value),
|
||||
} => write!(f, "{keyword}={}", XText(value)),
|
||||
RcptParameter::Other {
|
||||
keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
use super::*;
|
||||
use crate::transport::smtp::response::{Category, Code, Detail, Severity};
|
||||
|
||||
use super::{Extension, ServerInfo};
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::smtp::response::{Category, Code, Response, Severity};
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::Domain("test".to_owned())),
|
||||
"test".to_owned()
|
||||
);
|
||||
assert_eq!(format!("{LOCALHOST_CLIENT}"), "[127.0.0.1]".to_owned());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string());
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string());
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_owned()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,79 +319,97 @@ mod test {
|
||||
let mut eightbitmime = HashSet::new();
|
||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
}),
|
||||
"name with {EightBitMime}".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_owned(),
|
||||
features: eightbitmime,
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_owned()
|
||||
);
|
||||
|
||||
let empty = HashSet::new();
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}),
|
||||
"name with no supported features".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_owned(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_owned()
|
||||
);
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
}),
|
||||
"name with {Authentication(Plain)}".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_owned(),
|
||||
features: plain,
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo() {
|
||||
let response = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
1),
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
let response = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec!["me".to_owned(), "8BITMIME".to_owned(), "SIZE 42".to_owned()],
|
||||
);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features: features,
|
||||
name: "me".to_owned(),
|
||||
features,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||
|
||||
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
||||
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||
assert!(!server_info.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
assert!(server_info.supports_feature(Extension::EightBitMime));
|
||||
assert!(!server_info.supports_feature(Extension::StartTls));
|
||||
|
||||
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
1),
|
||||
vec!["me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
let response2 = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_owned(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_owned(),
|
||||
"8BITMIME".to_owned(),
|
||||
"SIZE 42".to_owned(),
|
||||
],
|
||||
);
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::CramMd5)));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
name: "me".to_owned(),
|
||||
features: features2,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||
|
||||
assert!(server_info2.supports_feature(&Extension::EightBitMime));
|
||||
assert!(server_info2.supports_feature(Extension::EightBitMime));
|
||||
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
|
||||
assert!(server_info2.supports_auth_mechanism(Mechanism::CramMd5));
|
||||
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||
assert!(!server_info2.supports_feature(Extension::StartTls));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,401 +1,272 @@
|
||||
//! Sends an email using the client
|
||||
//! The SMTP transport sends emails using the SMTP protocol.
|
||||
//!
|
||||
//! This SMTP client follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
|
||||
//! application to a relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC compliance checks.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](https://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](https://tools.ietf.org/html/rfc2487))
|
||||
//!
|
||||
//! #### SMTP Transport
|
||||
//!
|
||||
//! This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
//!
|
||||
//! It is designed to be:
|
||||
//!
|
||||
//! * Secured: connections are encrypted by default
|
||||
//! * Modern: unicode support for email contents and sender/recipient addresses when compatible
|
||||
//! * Fast: supports connection reuse and pooling
|
||||
//!
|
||||
//! This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
//! emails directly to the destination server.
|
||||
//!
|
||||
//! The relay server can be the local email server, a specific host or a third-party service.
|
||||
//!
|
||||
//! #### Simple example with authentication
|
||||
//!
|
||||
//! A good starting point for sending emails via SMTP relay is to
|
||||
//! do the following:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new(
|
||||
//! "username".to_owned(),
|
||||
//! "password".to_owned(),
|
||||
//! ))
|
||||
//! // Optionally configure expected authentication mechanism
|
||||
//! .authentication(vec![Mechanism::Plain])
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Shortening configuration
|
||||
//!
|
||||
//! It can be very repetitive to ask the user for every SMTP connection parameter.
|
||||
//! In some cases this can be simplified by using a connection URI instead.
|
||||
//!
|
||||
//! For more information take a look at [`SmtpTransport::from_url`] or [`AsyncSmtpTransport::from_url`].
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::authentication::{Credentials, Mechanism},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Advanced configuration with custom TLS settings
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use std::fs;
|
||||
//!
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::client::{Certificate, Tls, TlsParameters},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Custom TLS configuration - Use a self signed certificate
|
||||
//! let cert = fs::read("self-signed.crt")?;
|
||||
//! let cert = Certificate::from_pem(&cert)?;
|
||||
//! let tls = TlsParameters::builder(/* TLS SNI value */ "smtp.example.com".to_owned())
|
||||
//! .add_root_certificate(cert)
|
||||
//! .build()?;
|
||||
//!
|
||||
//! // Create the SMTPS transport
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")?
|
||||
//! .tls(Tls::Wrapper(tls))
|
||||
//! .build();
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! sender.send(&email)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! #### Connection pooling
|
||||
//!
|
||||
//! [`SmtpTransport`] and [`AsyncSmtpTransport`] store connections in
|
||||
//! a connection pool by default. This avoids connecting and disconnecting
|
||||
//! from the relay server for every message the application tries to send. For the connection pool
|
||||
//! to work the instance of the transport **must** be reused.
|
||||
//! In a webserver context it may go about this:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls")))]
|
||||
//! # fn test() {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType,
|
||||
//! transport::smtp::{authentication::Credentials, PoolConfig},
|
||||
//! Message, SmtpTransport, Transport,
|
||||
//! };
|
||||
//! #
|
||||
//! # type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
//!
|
||||
//! /// The global application state
|
||||
//! #[derive(Debug)]
|
||||
//! struct AppState {
|
||||
//! smtp: SmtpTransport,
|
||||
//! // ... other global application parameters
|
||||
//! }
|
||||
//!
|
||||
//! impl AppState {
|
||||
//! pub fn new(smtp_url: &str) -> Result<Self> {
|
||||
//! let smtp = SmtpTransport::from_url(smtp_url)?.build();
|
||||
//! Ok(Self { smtp })
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! fn handle_request(app_state: &AppState) -> Result<String> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Send the email via remote relay
|
||||
//! app_state.smtp.send(&email)?;
|
||||
//!
|
||||
//! Ok("The email has successfully been sent!".to_owned())
|
||||
//! }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::string::String;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
|
||||
use openssl::ssl::{SslContext, SslMethod};
|
||||
use client::Tls;
|
||||
|
||||
use transport::error::{EmailResult, Error};
|
||||
use transport::smtp::extension::{Extension, ServerInfo};
|
||||
use transport::smtp::client::Client;
|
||||
use transport::smtp::authentication::Mechanism;
|
||||
use transport::EmailTransport;
|
||||
use email::SendableEmail;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{AsyncSmtpTransport, AsyncSmtpTransportBuilder};
|
||||
#[cfg(feature = "pool")]
|
||||
pub use self::pool::PoolConfig;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||
};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use crate::transport::smtp::client::TlsParameters;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
client::SmtpConnection,
|
||||
extension::ClientId,
|
||||
response::Response,
|
||||
};
|
||||
|
||||
pub mod extension;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod response;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
mod connection_url;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "pool")]
|
||||
mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
pub(super) mod util;
|
||||
|
||||
// Registrated port numbers:
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub static SMTP_PORT: u16 = 25;
|
||||
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
/// Default submission port
|
||||
pub static SUBMISSION_PORT: u16 = 587;
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
///
|
||||
/// Defined in [RFC8314](https://tools.ietf.org/html/rfc8314)
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
// Useful strings and characters
|
||||
/// Default timeout
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
/// The word separator for SMTP transactions
|
||||
pub static SP: &'static str = " ";
|
||||
|
||||
/// The line ending for SMTP transactions (carriage return + line feed)
|
||||
pub static CRLF: &'static str = "\r\n";
|
||||
|
||||
/// Colon
|
||||
pub static COLON: &'static str = ":";
|
||||
|
||||
/// The ending of message content
|
||||
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
|
||||
|
||||
/// NUL unicode character
|
||||
pub static NUL: &'static str = "\0";
|
||||
|
||||
/// TLS security level
|
||||
#[derive(Debug)]
|
||||
pub enum SecurityLevel {
|
||||
/// Use a TLS wrapped connection
|
||||
///
|
||||
/// Non RFC-compliant, should only be used if the server does not support STARTTLS.
|
||||
EncryptedWrapper,
|
||||
/// Only send an email on encrypted connection (with STARTTLS)
|
||||
///
|
||||
/// Recommended mode, prevents MITM when used with verified certificates.
|
||||
AlwaysEncrypt,
|
||||
/// Use TLS when available (with STARTTLS)
|
||||
///
|
||||
/// Default mode.
|
||||
Opportunistic,
|
||||
/// Never use TLS
|
||||
NeverEncrypt,
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
pub struct SmtpTransportBuilder {
|
||||
/// Maximum connection reuse
|
||||
///
|
||||
/// Zero means no limitation
|
||||
connection_reuse_count_limit: u16,
|
||||
/// Enable connection reuse
|
||||
connection_reuse: bool,
|
||||
/// Name sent during HELO or EHLO
|
||||
hello_name: String,
|
||||
#[derive(Debug, Clone)]
|
||||
struct SmtpInfo {
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Server we are connecting to
|
||||
server: String,
|
||||
/// Port to connect to
|
||||
port: u16,
|
||||
/// TLS security configuration
|
||||
tls: Tls,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication: Vec<Mechanism>,
|
||||
/// Credentials
|
||||
credentials: Option<(String, String)>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// SSL contexyt to use
|
||||
ssl_context: SslContext,
|
||||
/// TLS security level
|
||||
security_level: SecurityLevel,
|
||||
/// Enable UTF8 mailboxes in enveloppe or headers
|
||||
smtp_utf8: bool,
|
||||
/// List of authentication mechanism, sorted by priority
|
||||
authentication_mechanisms: Vec<Mechanism>,
|
||||
credentials: Option<Credentials>,
|
||||
/// Define network timeout
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP SmtpTransport
|
||||
impl SmtpTransportBuilder {
|
||||
/// Creates a new local SMTP client
|
||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SmtpTransportBuilder, Error> {
|
||||
let mut addresses = try!(addr.to_socket_addrs());
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpTransportBuilder {
|
||||
server_addr: addr,
|
||||
ssl_context: SslContext::new(SslMethod::Tlsv1).unwrap(),
|
||||
security_level: SecurityLevel::Opportunistic,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse_count_limit: 100,
|
||||
connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
authentication_mechanisms: vec![Mechanism::CramMd5, Mechanism::Plain],
|
||||
}),
|
||||
None => Err(From::from("Could nor resolve hostname")),
|
||||
impl Default for SmtpInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: "localhost".to_owned(),
|
||||
port: SMTP_PORT,
|
||||
hello_name: ClientId::default(),
|
||||
credentials: None,
|
||||
authentication: DEFAULT_MECHANISMS.into(),
|
||||
timeout: Some(DEFAULT_TIMEOUT),
|
||||
tls: Tls::None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn localhost() -> Result<SmtpTransportBuilder, Error> {
|
||||
SmtpTransportBuilder::new(("localhost", SMTP_PORT))
|
||||
}
|
||||
|
||||
/// Use STARTTLS with a specific context
|
||||
pub fn ssl_context(mut self, ssl_context: SslContext) -> SmtpTransportBuilder {
|
||||
self.ssl_context = ssl_context;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the security level for SSL/TLS
|
||||
pub fn security_level(mut self, level: SecurityLevel) -> SmtpTransportBuilder {
|
||||
self.security_level = level;
|
||||
self
|
||||
}
|
||||
|
||||
/// Require SSL/TLS using STARTTLS
|
||||
///
|
||||
/// Incompatible with `ssl_wrapper()``
|
||||
pub fn encrypt(mut self) -> SmtpTransportBuilder {
|
||||
self.security_level = SecurityLevel::AlwaysEncrypt;
|
||||
self
|
||||
}
|
||||
|
||||
/// Require SSL/TLS using STARTTLS
|
||||
///
|
||||
/// Incompatible with `encrypt()`
|
||||
pub fn ssl_wrapper(mut self) -> SmtpTransportBuilder {
|
||||
self.security_level = SecurityLevel::EncryptedWrapper;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable SMTPUTF8 if the server supports it
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpTransportBuilder {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name used during HELO or EHLO
|
||||
pub fn hello_name(mut self, name: &str) -> SmtpTransportBuilder {
|
||||
self.hello_name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(mut self, enable: bool) -> SmtpTransportBuilder {
|
||||
self.connection_reuse = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum number of emails sent using one connection
|
||||
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SmtpTransportBuilder {
|
||||
self.connection_reuse_count_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials(mut self, username: &str, password: &str) -> SmtpTransportBuilder {
|
||||
self.credentials = Some((username.to_string(), password.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanisms
|
||||
pub fn authentication_mechanisms(mut self, mechanisms: Vec<Mechanism>) -> SmtpTransportBuilder {
|
||||
self.authentication_mechanisms = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `SmtpTransport`
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
SmtpTransport::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a client
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
/// Panic state
|
||||
pub panic: bool,
|
||||
/// Connection reuse counter
|
||||
pub connection_reuse_count: u16,
|
||||
}
|
||||
|
||||
/// Structure that implements the high level SMTP client
|
||||
pub struct SmtpTransport {
|
||||
/// Information about the server
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<ServerInfo>,
|
||||
/// SmtpTransport variable states
|
||||
state: State,
|
||||
/// Information about the client
|
||||
client_info: SmtpTransportBuilder,
|
||||
/// Low level client
|
||||
client: Client,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.reset();
|
||||
}
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpTransportBuilder) -> SmtpTransport {
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
SmtpTransport {
|
||||
client: client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the client state
|
||||
fn reset(&mut self) {
|
||||
// Close the SMTP transaction if needed
|
||||
self.close();
|
||||
|
||||
// Reset the client state
|
||||
self.server_info = None;
|
||||
self.state.panic = false;
|
||||
self.state.connection_reuse_count = 0;
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
pub fn get_ehlo(&mut self) -> EmailResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
|
||||
|
||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||
|
||||
// Print server information
|
||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||
|
||||
Ok(ehlo_response)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailTransport for SmtpTransport {
|
||||
/// Sends an email
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
|
||||
|
||||
// Extract email information
|
||||
let message_id = email.message_id();
|
||||
let from_address = email.from_address();
|
||||
let to_addresses = email.to_addresses();
|
||||
let message = email.message();
|
||||
|
||||
// Check if the connection is still available
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
if !self.client.is_connected() {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count == 0 {
|
||||
try!(self.client.connect(&self.client_info.server_addr,
|
||||
match &self.client_info.security_level {
|
||||
&SecurityLevel::EncryptedWrapper =>
|
||||
Some(&self.client_info.ssl_context),
|
||||
_ => None,
|
||||
}));
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
try!(self.get_ehlo());
|
||||
|
||||
match (&self.client_info.security_level,
|
||||
self.server_info.as_ref().unwrap().supports_feature(&Extension::StartTls)) {
|
||||
(&SecurityLevel::AlwaysEncrypt, false) =>
|
||||
return Err(From::from("Could not encrypt connection, aborting")),
|
||||
(&SecurityLevel::Opportunistic, false) => (),
|
||||
(&SecurityLevel::NeverEncrypt, _) => (),
|
||||
(&SecurityLevel::EncryptedWrapper, _) => (),
|
||||
(_, true) => {
|
||||
try_smtp!(self.client.starttls(), self);
|
||||
try_smtp!(self.client.upgrade_tls_stream(&self.client_info.ssl_context),
|
||||
self);
|
||||
|
||||
debug!("connection encrypted");
|
||||
|
||||
// Send EHLO again
|
||||
try!(self.get_ehlo());
|
||||
}
|
||||
}
|
||||
|
||||
if self.client_info.credentials.is_some() {
|
||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||
|
||||
let mut found = false;
|
||||
|
||||
for mechanism in self.client_info.authentication_mechanisms.clone() {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mechanism(mechanism) {
|
||||
found = true;
|
||||
try_smtp!(self.client.auth(mechanism, &username, &password), self);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mail_options = match (self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::EightBitMime),
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::SmtpUtfEight)) {
|
||||
(true, true) => Some("BODY=8BITMIME SMTPUTF8"),
|
||||
(true, false) => Some("BODY=8BITMIME"),
|
||||
(false, _) => None,
|
||||
};
|
||||
|
||||
try_smtp!(self.client.mail(&from_address, mail_options), self);
|
||||
|
||||
// Log the mail command
|
||||
info!("{}: from=<{}>", message_id, from_address);
|
||||
|
||||
// Recipient
|
||||
for to_address in to_addresses.iter() {
|
||||
try_smtp!(self.client.rcpt(&to_address), self);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.data(), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(&message);
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
||||
|
||||
// Log the message
|
||||
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
message.len(),
|
||||
result.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string()));
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
if (!self.client_info.connection_reuse) ||
|
||||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
|
||||
self.reset();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Closes the inner connection
|
||||
fn close(&mut self) {
|
||||
self.client.close();
|
||||
}
|
||||
}
|
||||
|
||||
342
src/transport/smtp/pool/async_impl.rs
Normal file
342
src/transport/smtp/pool/async_impl.rs
Normal file
@@ -0,0 +1,342 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{Arc, OnceLock},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use futures_util::{
|
||||
lock::Mutex,
|
||||
stream::{self, StreamExt},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{client::AsyncSmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::{
|
||||
executor::SpawnHandle,
|
||||
transport::smtp::{async_transport::AsyncSmtpClient, error},
|
||||
Executor,
|
||||
};
|
||||
|
||||
pub(crate) struct Pool<E: Executor> {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||
client: AsyncSmtpClient<E>,
|
||||
handle: OnceLock<E::Handle>,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: AsyncSmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub(crate) struct PooledConnection<E: Executor> {
|
||||
conn: Option<AsyncSmtpConnection>,
|
||||
pool: Arc<Pool<E>>,
|
||||
}
|
||||
|
||||
impl<E: Executor> Pool<E> {
|
||||
pub(crate) fn new(config: PoolConfig, client: AsyncSmtpClient<E>) -> Arc<Self> {
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Some(Vec::new())),
|
||||
client,
|
||||
handle: OnceLock::new(),
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
let handle = E::spawn(async move {
|
||||
loop {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
match pool.upgrade() {
|
||||
Some(pool) => {
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..(min_idle as usize) {
|
||||
let conn = match pool.client.connection().await {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
abort_concurrent(dropped.into_iter().map(ParkedConnection::unpark))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!(
|
||||
"breaking out of task - no more references to Pool are available"
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
E::sleep(idle_timeout).await;
|
||||
}
|
||||
});
|
||||
pool_
|
||||
.handle
|
||||
.set(handle)
|
||||
.expect("handle hasn't been set yet");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub(crate) async fn shutdown(&self) {
|
||||
let connections = { self.connections.lock().await.take() };
|
||||
if let Some(connections) = connections {
|
||||
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||
}
|
||||
|
||||
if let Some(handle) = self.handle.get() {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn connection(self: &Arc<Self>) -> Result<PooledConnection<E>, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().await;
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return Err(error::transport_shutdown());
|
||||
};
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected().await {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort().await;
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection().await?;
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn recycle(&self, mut conn: AsyncSmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort().await;
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections_guard = self.connections.lock().await;
|
||||
|
||||
if let Some(connections) = connections_guard.as_mut() {
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections_guard);
|
||||
conn.abort().await;
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
} else {
|
||||
// The pool has already been shut down
|
||||
drop(connections_guard);
|
||||
conn.abort().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Debug for Pool<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Some(connections) => {
|
||||
if let Some(connections) = connections.as_ref() {
|
||||
format!("{} connections", connections.len())
|
||||
} else {
|
||||
"SHUT DOWN".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
None => "LOCKED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.field(
|
||||
"handle",
|
||||
&match self.handle.get() {
|
||||
Some(_) => "Some(JoinHandle)",
|
||||
None => "None",
|
||||
},
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for Pool<E> {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
let connections = self.connections.get_mut().take();
|
||||
let handle = self.handle.take();
|
||||
E::spawn(async move {
|
||||
if let Some(handle) = handle {
|
||||
handle.shutdown().await;
|
||||
}
|
||||
|
||||
if let Some(connections) = connections {
|
||||
abort_concurrent(connections.into_iter().map(ParkedConnection::unpark)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: AsyncSmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> AsyncSmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> PooledConnection<E> {
|
||||
fn wrap(conn: AsyncSmtpConnection, pool: Arc<Pool<E>>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Deref for PooledConnection<E> {
|
||||
type Target = AsyncSmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> DerefMut for PooledConnection<E> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: Executor> Drop for PooledConnection<E> {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("AsyncSmtpConnection hasn't been taken yet");
|
||||
let pool = Arc::clone(&self.pool);
|
||||
|
||||
E::spawn(async move {
|
||||
pool.recycle(conn).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async fn abort_concurrent<I>(iter: I)
|
||||
where
|
||||
I: Iterator<Item = AsyncSmtpConnection>,
|
||||
{
|
||||
stream::iter(iter)
|
||||
.for_each_concurrent(8, |mut conn| async move {
|
||||
conn.abort().await;
|
||||
})
|
||||
.await;
|
||||
}
|
||||
66
src/transport/smtp/pool/mod.rs
Normal file
66
src/transport/smtp/pool/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub(super) mod async_impl;
|
||||
pub(super) mod sync_impl;
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Create a new pool configuration with default values
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.max_size = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
#[doc(hidden)]
|
||||
#[deprecated(note = "The Connection timeout is already configured on the SMTP transport")]
|
||||
pub fn connection_timeout(self, connection_timeout: Duration) -> Self {
|
||||
let _ = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
308
src/transport/smtp/pool/sync_impl.rs
Normal file
308
src/transport/smtp/pool/sync_impl.rs
Normal file
@@ -0,0 +1,308 @@
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
ops::{Deref, DerefMut},
|
||||
sync::{mpsc, Arc, Mutex, TryLockError},
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{client::SmtpConnection, Error},
|
||||
PoolConfig,
|
||||
};
|
||||
use crate::transport::smtp::{error, transport::SmtpClient};
|
||||
|
||||
pub(crate) struct Pool {
|
||||
config: PoolConfig,
|
||||
connections: Mutex<Option<Vec<ParkedConnection>>>,
|
||||
thread_terminator: mpsc::SyncSender<()>,
|
||||
client: SmtpClient,
|
||||
}
|
||||
|
||||
struct ParkedConnection {
|
||||
conn: SmtpConnection,
|
||||
since: Instant,
|
||||
}
|
||||
|
||||
pub(crate) struct PooledConnection {
|
||||
conn: Option<SmtpConnection>,
|
||||
pool: Arc<Pool>,
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
pub(crate) fn new(config: PoolConfig, client: SmtpClient) -> Arc<Self> {
|
||||
let (thread_tx, thread_rx) = mpsc::sync_channel(1);
|
||||
|
||||
let pool = Arc::new(Self {
|
||||
config,
|
||||
connections: Mutex::new(Some(Vec::new())),
|
||||
thread_terminator: thread_tx,
|
||||
client,
|
||||
});
|
||||
|
||||
{
|
||||
let pool_ = Arc::clone(&pool);
|
||||
|
||||
let min_idle = pool_.config.min_idle;
|
||||
let idle_timeout = pool_.config.idle_timeout;
|
||||
let pool = Arc::downgrade(&pool_);
|
||||
|
||||
thread::Builder::new()
|
||||
.name("lettre-connection-pool".into())
|
||||
.spawn(move || {
|
||||
while let Some(pool) = pool.upgrade() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::trace!("running cleanup tasks");
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
let (count, dropped) = {
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
let to_drop = connections
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.filter(|(_, conn)| conn.idle_duration() > idle_timeout)
|
||||
.map(|(i, _)| i)
|
||||
.collect::<Vec<_>>();
|
||||
let dropped = to_drop
|
||||
.into_iter()
|
||||
.map(|i| connections.remove(i))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
(connections.len(), dropped)
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
let mut created = 0;
|
||||
for _ in count..(min_idle as usize) {
|
||||
let conn = match pool.client.connection() {
|
||||
Ok(conn) => conn,
|
||||
Err(err) => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::warn!("couldn't create idle connection {}", err);
|
||||
#[cfg(not(feature = "tracing"))]
|
||||
let _ = err;
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let mut connections = pool.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return;
|
||||
};
|
||||
|
||||
connections.push(ParkedConnection::park(conn));
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
created += 1;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
if created > 0 {
|
||||
tracing::debug!("created {} idle connections", created);
|
||||
}
|
||||
|
||||
if !dropped.is_empty() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropped {} idle connections", dropped.len());
|
||||
|
||||
for conn in dropped {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
|
||||
drop(pool);
|
||||
|
||||
match thread_rx.recv_timeout(idle_timeout) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
// The transport was shut down
|
||||
return;
|
||||
}
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("couldn't spawn the Pool thread");
|
||||
}
|
||||
|
||||
pool
|
||||
}
|
||||
|
||||
pub(crate) fn shutdown(&self) {
|
||||
let connections = { self.connections.lock().unwrap().take() };
|
||||
if let Some(connections) = connections {
|
||||
for conn in connections {
|
||||
conn.unpark().abort();
|
||||
}
|
||||
}
|
||||
|
||||
_ = self.thread_terminator.try_send(());
|
||||
}
|
||||
|
||||
pub(crate) fn connection(self: &Arc<Self>) -> Result<PooledConnection, Error> {
|
||||
loop {
|
||||
let conn = {
|
||||
let mut connections = self.connections.lock().unwrap();
|
||||
let Some(connections) = connections.as_mut() else {
|
||||
// The transport was shut down
|
||||
return Err(error::transport_shutdown());
|
||||
};
|
||||
connections.pop()
|
||||
};
|
||||
|
||||
match conn {
|
||||
Some(conn) => {
|
||||
let mut conn = conn.unpark();
|
||||
|
||||
// TODO: handle the client try another connection if this one isn't good
|
||||
if !conn.test_connected() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection");
|
||||
|
||||
conn.abort();
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("reusing a pooled connection");
|
||||
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
None => {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("creating a new connection");
|
||||
|
||||
let conn = self.client.connection()?;
|
||||
return Ok(PooledConnection::wrap(conn, Arc::clone(self)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recycle(&self, mut conn: SmtpConnection) {
|
||||
if conn.has_broken() {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping a broken connection instead of recycling it");
|
||||
|
||||
conn.abort();
|
||||
drop(conn);
|
||||
} else {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("recycling connection");
|
||||
|
||||
let mut connections_guard = self.connections.lock().unwrap();
|
||||
|
||||
if let Some(connections) = connections_guard.as_mut() {
|
||||
if connections.len() >= self.config.max_size as usize {
|
||||
drop(connections_guard);
|
||||
conn.abort();
|
||||
} else {
|
||||
let conn = ParkedConnection::park(conn);
|
||||
connections.push(conn);
|
||||
}
|
||||
} else {
|
||||
// The pool has already been shut down
|
||||
drop(connections_guard);
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for Pool {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Pool")
|
||||
.field("config", &self.config)
|
||||
.field(
|
||||
"connections",
|
||||
&match self.connections.try_lock() {
|
||||
Ok(connections) => {
|
||||
if let Some(connections) = connections.as_ref() {
|
||||
format!("{} connections", connections.len())
|
||||
} else {
|
||||
"SHUT DOWN".to_owned()
|
||||
}
|
||||
}
|
||||
|
||||
Err(TryLockError::WouldBlock) => "LOCKED".to_owned(),
|
||||
Err(TryLockError::Poisoned(_)) => "POISONED".to_owned(),
|
||||
},
|
||||
)
|
||||
.field("client", &self.client)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Pool {
|
||||
fn drop(&mut self) {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("dropping Pool");
|
||||
|
||||
if let Some(connections) = self.connections.get_mut().unwrap().take() {
|
||||
for conn in connections {
|
||||
let mut conn = conn.unpark();
|
||||
conn.abort();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParkedConnection {
|
||||
fn park(conn: SmtpConnection) -> Self {
|
||||
Self {
|
||||
conn,
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn idle_duration(&self) -> Duration {
|
||||
self.since.elapsed()
|
||||
}
|
||||
|
||||
fn unpark(self) -> SmtpConnection {
|
||||
self.conn
|
||||
}
|
||||
}
|
||||
|
||||
impl PooledConnection {
|
||||
fn wrap(conn: SmtpConnection, pool: Arc<Pool>) -> Self {
|
||||
Self {
|
||||
conn: Some(conn),
|
||||
pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PooledConnection {
|
||||
type Target = SmtpConnection;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.conn.as_ref().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PooledConnection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
self.conn.as_mut().expect("conn hasn't been dropped yet")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for PooledConnection {
|
||||
fn drop(&mut self) {
|
||||
let conn = self
|
||||
.conn
|
||||
.take()
|
||||
.expect("SmtpConnection hasn't been taken yet");
|
||||
self.pool.recycle(conn);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
495
src/transport/smtp/transport.rs
Normal file
495
src/transport/smtp/transport.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
#[cfg(feature = "pool")]
|
||||
use std::sync::Arc;
|
||||
use std::{fmt::Debug, time::Duration};
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
use super::pool::sync_impl::Pool;
|
||||
#[cfg(feature = "pool")]
|
||||
use super::PoolConfig;
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
/// Synchronously send emails using the SMTP protocol
|
||||
///
|
||||
/// `SmtpTransport` is the primary way for communicating
|
||||
/// with SMTP relay servers to send email messages. It holds the
|
||||
/// client connect configuration and creates new connections
|
||||
/// as necessary.
|
||||
///
|
||||
/// # Connection pool
|
||||
///
|
||||
/// When the `pool` feature is enabled (default), `SmtpTransport` maintains a
|
||||
/// connection pool to manage SMTP connections. The pool:
|
||||
///
|
||||
/// - Establishes a new connection when sending a message.
|
||||
/// - Recycles connections internally after a message is sent.
|
||||
/// - Reuses connections for subsequent messages, reducing connection setup overhead.
|
||||
///
|
||||
/// The connection pool can grow to hold multiple SMTP connections if multiple
|
||||
/// emails are sent concurrently, as SMTP does not support multiplexing within a
|
||||
/// single connection.
|
||||
///
|
||||
/// However, **connection reuse is not possible** if the `SmtpTransport` instance
|
||||
/// is dropped after every email send operation. You must reuse the instance
|
||||
/// of this struct for the connection pool to be of any use.
|
||||
///
|
||||
/// To customize connection pool settings, use [`SmtpTransportBuilder::pool_config`].
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "pool")]
|
||||
inner: Arc<Pool>,
|
||||
#[cfg(not(feature = "pool"))]
|
||||
inner: SmtpClient,
|
||||
}
|
||||
|
||||
impl Transport for SmtpTransport {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.abort();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn shutdown(&self) {
|
||||
#[cfg(feature = "pool")]
|
||||
self.inner.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for SmtpTransport {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut builder = f.debug_struct("SmtpTransport");
|
||||
builder.field("inner", &self.inner);
|
||||
builder.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple and secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> SmtpTransport {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60-seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
SmtpTransportBuilder::new(server)
|
||||
}
|
||||
|
||||
/// Creates a `SmtpTransportBuilder` from a connection URL
|
||||
///
|
||||
/// The protocol, credentials, host, port and EHLO name can be provided
|
||||
/// in a single URL. This may be simpler than having to configure SMTP
|
||||
/// through multiple configuration parameters and then having to pass
|
||||
/// those options to lettre.
|
||||
///
|
||||
/// The URL is created in the following way:
|
||||
/// `scheme://user:pass@hostname:port/ehlo-name?tls=TLS`.
|
||||
///
|
||||
/// `user` (Username) and `pass` (Password) are optional in case the
|
||||
/// SMTP relay doesn't require authentication. When `port` is not
|
||||
/// configured it is automatically determined based on the `scheme`.
|
||||
/// `ehlo-name` optionally overwrites the hostname sent for the EHLO
|
||||
/// command. `TLS` controls whether STARTTLS is simply enabled
|
||||
/// (`opportunistic` - not enough to prevent man-in-the-middle attacks)
|
||||
/// or `required` (require the server to upgrade the connection to
|
||||
/// STARTTLS, otherwise fail on suspicion of main-in-the-middle attempt).
|
||||
///
|
||||
/// Use the following table to construct your SMTP url:
|
||||
///
|
||||
/// | scheme | `tls` query parameter | example | default port | remarks |
|
||||
/// | ------- | --------------------- | -------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
/// | `smtps` | unset | `smtps://user:pass@hostname:port` | 465 | SMTP over TLS, recommended method |
|
||||
/// | `smtp` | `required` | `smtp://user:pass@hostname:port?tls=required` | 587 | SMTP with STARTTLS required, when SMTP over TLS is not available |
|
||||
/// | `smtp` | `opportunistic` | `smtp://user:pass@hostname:port?tls=opportunistic` | 587 | SMTP with optionally STARTTLS when supported by the server. Not suitable for production use: vulnerable to a man-in-the-middle attack |
|
||||
/// | `smtp` | unset | `smtp://user:pass@hostname:port` | 587 | Always unencrypted SMTP. Not suitable for production use: sends all data unencrypted |
|
||||
///
|
||||
/// IMPORTANT: some parameters like `user` and `pass` cannot simply
|
||||
/// be concatenated to construct the final URL because special characters
|
||||
/// contained within the parameter may confuse the URL decoder.
|
||||
/// Manually URL encode the parameters before concatenating them or use
|
||||
/// a proper URL encoder, like the following cargo script:
|
||||
///
|
||||
/// ```rust
|
||||
/// # const TOML: &str = r#"
|
||||
/// #!/usr/bin/env cargo
|
||||
///
|
||||
/// //! ```cargo
|
||||
/// //! [dependencies]
|
||||
/// //! url = "2"
|
||||
/// //! ```
|
||||
/// # "#;
|
||||
///
|
||||
/// use url::Url;
|
||||
///
|
||||
/// fn main() {
|
||||
/// // don't touch this line
|
||||
/// let mut url = Url::parse("foo://bar").unwrap();
|
||||
///
|
||||
/// // configure the scheme (`smtp` or `smtps`) here.
|
||||
/// url.set_scheme("smtps").unwrap();
|
||||
/// // configure the username and password.
|
||||
/// // remove the following two lines if unauthenticated.
|
||||
/// url.set_username("username").unwrap();
|
||||
/// url.set_password(Some("password")).unwrap();
|
||||
/// // configure the hostname
|
||||
/// url.set_host(Some("smtp.example.com")).unwrap();
|
||||
/// // configure the port - only necessary if using a non-default port
|
||||
/// url.set_port(Some(465)).unwrap();
|
||||
/// // configure the EHLO name
|
||||
/// url.set_path("ehlo-name");
|
||||
///
|
||||
/// println!("{url}");
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// The connection URL can then be used in the following way:
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use lettre::{
|
||||
/// message::header::ContentType, transport::smtp::authentication::Credentials, Message,
|
||||
/// SmtpTransport, Transport,
|
||||
/// };
|
||||
///
|
||||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let email = Message::builder()
|
||||
/// .from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
/// .reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
/// .to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
/// .subject("Happy new year")
|
||||
/// .header(ContentType::TEXT_PLAIN)
|
||||
/// .body(String::from("Be happy!"))
|
||||
/// .unwrap();
|
||||
///
|
||||
/// // Open a remote connection to example
|
||||
/// let mailer = SmtpTransport::from_url("smtps://username:password@smtp.example.com")?.build();
|
||||
///
|
||||
/// // Send the email
|
||||
/// mailer.send(&email)?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn from_url(connection_url: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
super::connection_url::from_connection_url(connection_url)
|
||||
}
|
||||
|
||||
/// Tests the SMTP connection
|
||||
///
|
||||
/// `test_connection()` tests the connection by using the SMTP NOOP command.
|
||||
/// The connection is closed afterward if a connection pool is not used.
|
||||
pub fn test_connection(&self) -> Result<bool, Error> {
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let is_connected = conn.test_connected();
|
||||
|
||||
#[cfg(not(feature = "pool"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(is_connected)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`SmtpTransport`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpTransportBuilder {
|
||||
// Create new builder with default parameters
|
||||
pub(crate) fn new<T: Into<String>>(server: T) -> Self {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
info: new,
|
||||
#[cfg(feature = "pool")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication credentials to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// lettre usually picks the correct `port` when building
|
||||
/// [`SmtpTransport`] using [`SmtpTransport::relay`] or
|
||||
/// [`SmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the incorrect `port` and [`Self::tls`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
///
|
||||
/// # ⚠️⚠️⚠️ You probably don't need to call this method ⚠️⚠️⚠️
|
||||
///
|
||||
/// By default lettre chooses the correct `tls` configuration when
|
||||
/// building [`SmtpTransport`] using [`SmtpTransport::relay`] or
|
||||
/// [`SmtpTransport::starttls_relay`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Using the wrong [`Tls`] and [`Self::port`] combination may
|
||||
/// lead to hard to debug IO errors coming from the TLS library.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
#[cfg_attr(
|
||||
docsrs,
|
||||
doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
|
||||
)]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "pool")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "pool")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
///
|
||||
/// If the `pool` feature is enabled, an `Arc` wrapped pool is created.
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = SmtpClient { info: self.info };
|
||||
|
||||
#[cfg(feature = "pool")]
|
||||
let client = Pool::new(self.pool_config, client);
|
||||
|
||||
SmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
impl SmtpClient {
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub(super) fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match &self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
Tls::Wrapper(tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut conn = SmtpConnection::connect::<(&str, u16)>(
|
||||
(self.info.server.as_ref(), self.info.port),
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
tls_parameters,
|
||||
None,
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
|
||||
match &self.info.tls {
|
||||
Tls::Opportunistic(tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
}
|
||||
Tls::Required(tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, credentials)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
transport::smtp::{authentication::Credentials, client::Tls},
|
||||
SmtpTransport,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn transport_from_url() {
|
||||
let builder = SmtpTransport::from_url("smtp://127.0.0.1:2525").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 2525);
|
||||
assert!(matches!(builder.info.tls, Tls::None));
|
||||
assert_eq!(builder.info.server, "127.0.0.1");
|
||||
|
||||
let builder =
|
||||
SmtpTransport::from_url("smtps://username:password@smtp.example.com:465").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"username".to_owned(),
|
||||
"password".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
assert_eq!(builder.info.server, "smtp.example.com");
|
||||
|
||||
let builder = SmtpTransport::from_url(
|
||||
"smtps://user%40example.com:pa$$word%3F%22!@smtp.example.com:465",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"user@example.com".to_owned(),
|
||||
"pa$$word?\"!".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
assert_eq!(builder.info.server, "smtp.example.com");
|
||||
|
||||
let builder =
|
||||
SmtpTransport::from_url("smtp://username:password@smtp.example.com:587?tls=required")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 587);
|
||||
assert_eq!(
|
||||
builder.info.credentials,
|
||||
Some(Credentials::new(
|
||||
"username".to_owned(),
|
||||
"password".to_owned()
|
||||
))
|
||||
);
|
||||
assert!(matches!(builder.info.tls, Tls::Required(_)));
|
||||
|
||||
let builder = SmtpTransport::from_url(
|
||||
"smtp://username:password@smtp.example.com:587?tls=opportunistic",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 587);
|
||||
assert!(matches!(builder.info.tls, Tls::Opportunistic(_)));
|
||||
|
||||
let builder = SmtpTransport::from_url("smtps://smtp.example.com").unwrap();
|
||||
|
||||
assert_eq!(builder.info.port, 465);
|
||||
assert_eq!(builder.info.credentials, None);
|
||||
assert!(matches!(builder.info.tls, Tls::Wrapper(_)));
|
||||
}
|
||||
}
|
||||
45
src/transport/smtp/util.rs
Normal file
45
src/transport/smtp/util.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
//! Utils for string manipulation
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct XText<'a>(pub(crate) &'a str);
|
||||
|
||||
impl Display for XText<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut rest = self.0;
|
||||
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
|
||||
let (start, end) = rest.split_at(idx);
|
||||
f.write_str(start)?;
|
||||
|
||||
let mut end_iter = end.char_indices();
|
||||
let (_, c) = end_iter.next().expect("char");
|
||||
write!(f, "+{:X}", c as u8)?;
|
||||
|
||||
if let Some((idx, _)) = end_iter.next() {
|
||||
rest = &end[idx..];
|
||||
} else {
|
||||
rest = "";
|
||||
}
|
||||
}
|
||||
f.write_str(rest)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::XText;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
for (input, expect) in [
|
||||
("bjorn", "bjorn"),
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
] {
|
||||
assert_eq!(format!("{}", XText(input)), (*expect).to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,178 @@
|
||||
//! This transport is a stub that only logs the message, and always returns
|
||||
//! succes
|
||||
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||
//! purposes.
|
||||
//!
|
||||
//! # Stub Transport
|
||||
//!
|
||||
//! The stub transport logs message envelopes as well as contents. It can be useful for testing
|
||||
//! purposes.
|
||||
//!
|
||||
//! # Examples
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "builder")]
|
||||
//! # {
|
||||
//! use lettre::{
|
||||
//! message::header::ContentType, transport::stub::StubTransport, Message, Transport,
|
||||
//! };
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn try_main() -> Result<(), Box<dyn Error>> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .header(ContentType::TEXT_PLAIN)
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! sender.send(&email)?;
|
||||
//! assert_eq!(
|
||||
//! sender.messages(),
|
||||
//! vec![(
|
||||
//! email.envelope().clone(),
|
||||
//! String::from_utf8(email.formatted()).unwrap()
|
||||
//! )],
|
||||
//! );
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # try_main().unwrap();
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use transport::error::EmailResult;
|
||||
use transport::smtp::response::{Category, Code, Response, Severity};
|
||||
use transport::EmailTransport;
|
||||
use email::SendableEmail;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// This transport does nothing exept logging the message enveloppe
|
||||
pub struct StubEmailTransport;
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
impl EmailTransport for StubEmailTransport {
|
||||
fn send<T: SendableEmail>(&mut self, email: T) -> EmailResult {
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
use crate::AsyncTransport;
|
||||
use crate::{address::Envelope, Transport};
|
||||
|
||||
info!("{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
email.from_address(),
|
||||
email.to_addresses());
|
||||
Ok(Response::new(Code::new(Severity::PositiveCompletion, Category::MailSystem, 0),
|
||||
vec!["Ok: email logged".to_string()]))
|
||||
}
|
||||
/// An error returned by the stub transport
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Error;
|
||||
|
||||
fn close(&mut self) {
|
||||
()
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("stub error")
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
|
||||
/// This transport logs messages and always returns the given response
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
/// This transport logs messages and always returns the given response
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "tokio1", feature = "async-std1"))))]
|
||||
pub struct AsyncStubTransport {
|
||||
response: Result<(), Error>,
|
||||
message_log: Arc<Mutex<Vec<(Envelope, String)>>>,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all logged messages sent using [`Transport::send_raw`]
|
||||
pub fn messages(&self) -> Vec<(Envelope, String)> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.expect("Couldn't acquire lock to write message log")
|
||||
.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio1"))]
|
||||
impl AsyncStubTransport {
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> Self {
|
||||
Self {
|
||||
response,
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> Self {
|
||||
Self {
|
||||
response: Ok(()),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> Self {
|
||||
Self {
|
||||
response: Err(Error),
|
||||
message_log: Arc::new(Mutex::new(vec![])),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return all logged messages sent using [`AsyncTransport::send_raw`]
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
pub async fn messages(&self) -> Vec<(Envelope, String)> {
|
||||
self.message_log.lock().unwrap().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.expect("Couldn't acquire lock to write message log")
|
||||
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio1", feature = "async-std1"))]
|
||||
#[async_trait]
|
||||
impl AsyncTransport for AsyncStubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.message_log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push((envelope.clone(), String::from_utf8_lossy(email).into()));
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
7
testdata/coredns.conf
vendored
Normal file
7
testdata/coredns.conf
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
. {
|
||||
bind 127.0.0.54
|
||||
forward . 9.9.9.9 8.8.8.8 1.1.1.1 {
|
||||
except example.org
|
||||
}
|
||||
file testdata/db.example.org example.org
|
||||
}
|
||||
2
testdata/db.example.org
vendored
Normal file
2
testdata/db.example.org
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
@ 600 IN SOA ns.example.org hostmaster.example.org 1 10800 3600 604800 3600
|
||||
dkimtest._domainkey 600 IN TXT "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+FHbM8BwkBBz/Ux5OYLQ5Bp1HVuCHTP6Rr3HXTnome/2cGl/ze0tsmmFbCjjsS89MXbMGs9xJhjv18LmL1N0UTllblOizzVjorQyN4RwBOfG34j7SS56pwzrA738Ry8FAbL5InPWEgVzbOhXuTCs8yuzcqTnm4sH/csnIl7cMWeQkVn1FR9LKMtUG0fjhDPkdX0jx3qTX1L3Z7a7gX6geY191yNd9i9DvE2/+wMigMYz1LAts4alk2g86MQhtbjc8AOR7EC15hSw37/lmamlunYLa3wC+PzHNMA8sAfnmkgNvipssjh8LnelD9qn+VtsjQB5ppkeQx3TcUPvz5z+QIDAQAB"
|
||||
234
testdata/email_with_png.eml
vendored
Normal file
234
testdata/email_with_png.eml
vendored
Normal file
@@ -0,0 +1,234 @@
|
||||
Date: Tue, 15 Nov 1994 08:12:31 +0000
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related;
|
||||
boundary="GUEEoEeTXtLcK2sMhmH1RfC1co13g4rtnRUFjQFA"
|
||||
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: image/png
|
||||
Content-Disposition: inline
|
||||
Content-ID: <123>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA
|
||||
GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO
|
||||
izAORCmKgKmKKQa1taUWl1C1cWm1rVvdSls01bbGpcsDTWOrMcabdNNiXdqkrSamMbY2hkTbOiyD
|
||||
0HFBiRSLE6WFKgoFFIRBztwP7M2//gEVnJnf95zzeiUmLjDziYlnztvrd12noaOjozsAAACAohpL
|
||||
DwAAAAAEOgAAAKQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ9Cs9ADL6wx/+EHfffXfpGQB7ZNiw
|
||||
YfGb3/ym9AwAYC8JdNiFN954I1atWlV6BsAeGT58eOkJAMAH4Ig7AAAAJCDQAQAAIAGBDgAAAAkI
|
||||
dAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhA
|
||||
oAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEAC
|
||||
Ah0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAAS
|
||||
EOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQ
|
||||
gEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACA
|
||||
BAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAA
|
||||
JCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAA
|
||||
IAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAA
|
||||
AAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAA
|
||||
AEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMA
|
||||
AEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0A
|
||||
AAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgA
|
||||
AACQgEAHAACABAQ6AAAAJCDQAQAAIIF+pQdARieeeGIcfPDBpWdQJ55//vn40Y9+VHoGAACFCXTY
|
||||
hf322y/222+/0jOoE01NTaUnAACQgCPuAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADFNbY6KUYAACBDlBcU1NT6QkAACQg0AEK69evX+kJAAAkINABCnMFHQCACIEOUJxABwAgQqAD
|
||||
FOeIOwAAEQIdoDhPcaenbN++vfQEAOAD8K4QoDBX0Okpb731VukJAMAHINABChPo9BSBDgDVTaAD
|
||||
FOYhcfSU7du3R3d3d+kZAMBeEugAhQl0epL70AGgegl0gML69+9fegI1pKurq/QEAGAvCXSAwgYP
|
||||
HhwNDQ2lZ1AjXn/99dITAIC9JNABCmtsbIyBAweWnkGNEOgAUL0EOkACzc3NpSdQIwQ6AFQvgQ6Q
|
||||
gECnpwh0AKheAh0ggSFDhpSeQI144403Sk8AAPaSQAdIwBV0esprr71WegIAsJcEOkACAp2esmbN
|
||||
mtITAIC9JNABEnDEnZ4i0AGgegl0gAQGDx5cegI1QqADQPUS6AAJuIJOTxHoAFC9BDpAAgKdnvLa
|
||||
a6/F1q1bS88AAPaCQAdIYPjw4aUnUCMqlUqsXLmy9AwAYC8IdIAEWlpaSk+ghrz44oulJwAAe0Gg
|
||||
AyQwcuTI0hOoIQIdAKqTQAdIQKDTkwQ6AFQngQ6QgCPu9KTly5eXngAA7AWBDpDAyJEjo7HRSzI9
|
||||
46WXXoo33nij9AwAYA95NwiQQGNjoye502O6u7tdRQeAKiTQAZJwzJ2e9Nxzz5WeAADsIYEOkMSI
|
||||
ESNKT6CGLF26tPQEAGAPCXSAJFxBpyd1dHREpVIpPQMA2AMCHSAJgU5P2rBhQ6xYsaL0DABgDwh0
|
||||
gCRaW1tLT6DGdHR0lJ4AAOwBgQ6QxJgxY0pPoMYsXry49AQAYA8IdIAk9t9//9ITqDEdHR3x5ptv
|
||||
lp4BALxPAh0gCYFOT9uyZUssWrSo9AwA4H0S6ABJtLa2Rv/+/UvPoMb85S9/KT0BAHifBDpAEo2N
|
||||
jTF69OjSM6gxc+fOjW3btpWeAQC8DwIdIBEPiqOnvf766x4WBwBVQqADJOI+dHrDQw89VHoCAPA+
|
||||
CHSARPbbb7/SE6hBbW1tsX79+tIzAID3INABEnEFnd6wbdu2mDNnTukZAMB7EOgAibgHnd7ywAMP
|
||||
RHd3d+kZAMC7EOgAiRx44IGlJ1CjOjs7Y+HChaVnAADvQqADJDJq1KgYOnRo6RnUqHvvvbf0BADg
|
||||
XQh0gGQOOuig0hOoUY8//ng8//zzpWcAALsh0AGSmTBhQukJ1LBf/epXpScAALsh0AGScQWd3vTI
|
||||
I4+4ig4ASQl0gGTGjx9fegI1rLu7O2bPnl16BgCwCwIdIBlH3Oltc+fOjWeffbb0DADg/yPQAZLZ
|
||||
f//9Y+DAgaVnUONuueWWqFQqpWcAAP9FoAMk09jYGOPGjSs9gxq3bNmyePDBB0vPAAD+i0AHSMiD
|
||||
4ugLP/vZz2L9+vWlZwAA/yHQARI6+OCDS0+gDmzcuDFmzZpVegYA8B8CHSChQw89tPQE6sTDDz8c
|
||||
jz76aOkZAEAIdICUJk2aVHoCdeTmm2+OtWvXlp4BAHVPoAMkNGLEiBgzZkzpGdSJ9evXxw033OCp
|
||||
7gBQmEAHSOqwww4rPYE6smDBgrjnnntKzwCAuibQAZIS6PS1X/ziF9He3l56BgDULYEOkJT70Olr
|
||||
lUolrrvuuli5cmXpKQBQlwQ6QFKTJk2KhoaG0jOoMxs2bIgrrrgi1q1bV3oKANQdgQ6Q1NChQ+OA
|
||||
Aw4oPYM6tHr16rjyyivjzTffLD0FAOqKQAdIzOehU8pzzz0X3/72t6Orq6v0FACoGwIdILHDDz+8
|
||||
9ATq2MKFC+Oaa64R6QDQRwQ6QGJTpkwpPYE6197eHtdee61IB4A+INABEjvkkENiyJAhpWdQ5xYs
|
||||
WBCXXnpprF+/vvQUAKhpAh0gsaampjjiiCNKz4B49tlnY+bMmfHyyy+XngIANUugAyR31FFHlZ4A
|
||||
ERHR2dkZM2bMiKVLl5aeAgA1SaADJHfkkUeWngA7rFmzJmbOnBkPPPBA6Skk0dXV5RkFAD1EoAMk
|
||||
d9hhh8XgwYNLz4Adurq64qabboof/OAHPiu9jj333HNxyy23xCmnnBJz584tPQegJgh0gOT69esX
|
||||
kydPLj0DdvL73/8+zjnnnFiyZEnpKfSRtWvXxn333Rfnn39+XHTRRXH//ffHxo0bBTpAD+lXegAA
|
||||
7+2oo46KRYsWlZ4BO1m9enVceuml8ZWvfCUuueSS6N+/f+lJ9LCurq6YP39+PPTQQ9He3h7bt2/f
|
||||
6dfMnz8/tm/fHk1NTQUWAtQOgQ5QBdyHTmZvvfVW3HHHHfHII4/E9773vZgyZUrpSfSAZcuWxR//
|
||||
+MeYM2dObNiw4V1/7caNG2PJkiVeqwA+IIEOUAUmTZoUgwYNii1btpSeArvV2dkZM2fOjNNPPz0u
|
||||
v/zy2HfffUtPYg8tW7Ys/vznP8ef/vSnWLly5R793ra2NoEO8AE1dHR0dJceAcB7u+qqq9znSdUY
|
||||
PHhwnH322TFjxowYMmRI6Tm8i87Oznj00Udjzpw5exzl/23s2LHx4IMP9uAygPoj0AGqxAMPPBA3
|
||||
3XRT6RmwR1pbW+Piiy+OM888MwYNGlR6DhFRqVTimWee2XGl/JVXXumxr33//ffHhAkTeuzrAdQb
|
||||
gQ5QJdasWROnnnpqdHd72ab6tLS0xAUXXBBf+MIXorm5ufScurN58+b461//GgsXLoy2trZ49dVX
|
||||
e+X7XHHFFXHJJZf0ytcGqAcCHaCKnH/++fH888+XngF7rbm5OU455ZQ499xzXWntRZVKJZYvXx6L
|
||||
Fy+ORYsWxVNPPRXbtm3r9e87efLkuOeee3r9+wDUKoEOUEVuv/12b36pCQ0NDXHMMcfE6aefHscf
|
||||
f3wMHDiw9KSq9+qrr8bjjz8eCxcujMWLF8f69ev7fENjY2PMmTMnWlpa+vx7A9QCgQ5QRZ555pmY
|
||||
MWNG6RnQo4YOHRrTp0+Pk08+OY466qhobGwsPSm97u7ueOmll+LZZ5+NZ555JpYsWRKdnZ2lZ0VE
|
||||
xHXXXRef/exnS88AqEoCHaCKVCqVOOmkk4pcGYO+MGLEiJg2bVqccMIJcfTRR8fgwYNLT0rhzTff
|
||||
jOeeey6WLFmyI8rf67PJS5k2bVrMmjWr9AyAqiTQAarM9ddfHw8//HDpGdDrBgwYEJMnT46Pf/zj
|
||||
cfTRR8ehhx4a/fr1Kz2r161bty5WrFgRnZ2d8cILL8SyZcvi73//e2zfvr30tPdl0KBB8eijj3pq
|
||||
P8BeqP2fcgA15thjjxXo1IWurq7o6OiIjo6OiHg7/CZNmhRHHHFEHHLIITFx4sQ44IADqjbaN27c
|
||||
GC+++GKsWLEiXnjhhejs7IwVK1bEv//979LTPpAtW7bE4sWLY9q0aaWnAFSd6vyJBlDHpk6dGgMG
|
||||
DIiurq7SU6BPbdmyJZ588sl48sknd/y9/v37x7hx42LChAkxceLEGD9+fIwfPz5Gjx4dQ4YMKba1
|
||||
UqnE2rVr41//+le8/PLL8fLLL+/489WrV8fLL78cmzZtKravt7W1tQl0gL0g0AGqzNChQ2Pq1Knx
|
||||
2GOPlZ4CxW3bti1WrFgRK1as2OmfDRw4MFpbW6O1tTX23XffaG1tjZaWlmhpaYmmpqYYMmRINDQ0
|
||||
xLBhwyLi7f+2Ghoa3vG1t2zZEhERW7duja1bt0bE2/eDb9q0KTZu3Ljjjw0bNsTrr78eGzZs2PHX
|
||||
lUqlD/4N5DRv3ryoVCoe+AewhwQ6QBU66aSTBDq8h61bt8aqVati1apVpafUnXXr1sXSpUtj8uTJ
|
||||
pacAVBX/WxOgCk2bNi2am5tLzwDYrXnz5pWeAFB1BDpAFRo0aFAce+yxpWcA7FZbW1vpCQBVR6AD
|
||||
VKmTTjqp9ASA3ers7IyVK1eWngFQVQQ6QJWaOnXqjodbAWTkmDvAnhHoAFVqwIABcfzxx5eeAbBb
|
||||
c+fOLT0BoKoIdIAq9ulPf7r0BIDdeuqpp2LDhg2lZwBUDYEOUMU+9rGPRUtLS+kZALtUqVRiwYIF
|
||||
pWcAVA2BDlDF+vXrF6eddlrpGQC75Zg7wPsn0AGq3FlnnRUNDQ2lZwDsUnt7e3R1dZWeAVAVBDpA
|
||||
lRs7dmwcddRRpWcA7NLmzZvjiSeeKD0DoCoIdIAacOaZZ5aeALBbPm4N4P0R6AA14MQTT4yRI0eW
|
||||
ngGwS4899lh0d3eXngGQnkAHqAH9+/ePU089tfQMgF1as2ZNLF++vPQMgPQEOkCNOPPMMz0sDkjL
|
||||
09wB3ptAB6gR48aNiylTppSeAbBLAh3gvQl0gBry+c9/vvQEgF1avnx5vPLKK6VnAKQm0AFqyPTp
|
||||
02P//fcvPQNgJ93d3a6iA7wHgQ5QQ5qamuK8884rPQNglwQ6wLsT6AA15nOf+1wMGzas9AyAnXR0
|
||||
dMSmTZtKzwBIS6AD1Jjm5uY466yzSs8A2Mm2bdti4cKFpWcApCXQAWrQl770pRgwYEDpGQA7ccwd
|
||||
YPcEOkANamlpiVNOOaX0DICdzJ8/P7Zv3156BkBKAh2gRl100UXR2OhlHshl48aN8fTTT5eeAZCS
|
||||
d24ANeqggw6KT3ziE6VnAOzEMXeAXRPoADVsxowZpScA7KStra30BICUBDpADTviiCNi6tSppWcA
|
||||
vMM///nP6OzsLD0DIB2BDlDjvv71r0dDQ0PpGQDv4Co6wM4EOkCNO/TQQ+OEE04oPQPgHebNm1d6
|
||||
AkA6Ah2gDlx++eXR1NRUegbADkuXLo21a9eWngGQikAHqAPjxo2Lk08+ufQMgB0qlUrMnz+/9AyA
|
||||
VAQ6QJ247LLLon///qVnAOzgPnSAdxLoAHVizJgxcfrpp5eeAbDDokWLYsuWLaVnAKQh0AHqyFe/
|
||||
+tUYPHhw6RkAERGxdevWWLx4cekZAGkIdIA6Mnr06Pjyl79cegbADo65A/w/Ah2gzlx44YVx4IEH
|
||||
lp4BEBFvf9xapVIpPQMgBYEOUGf69+8f3/rWt0rPAIiIiHXr1sXSpUtLzwBIQaAD1KHjjjsupk6d
|
||||
WnoGQES8fRUdAIEOULeuvfbaGDBgQOkZAO5DB/gPgQ5Qpw444IA455xzSs8AiM7Ozli5cmXpGQDF
|
||||
CXSAOva1r30tRo0aVXoGgGPuACHQAepac3NzfPOb3yw9AyDmzp1begJAcQIdoM595jOfiRNOOKH0
|
||||
DKDOPfXUU7Fhw4bSMwCKEugAxHe+850YPnwtwJqJAAAO10lEQVR46RlAHatUKrFgwYLSMwCKEugA
|
||||
RGtra1x11VWlZwB1zjF3oN4JdAAiIuK0006L4447rvQMoI61t7dHV1dX6RkAxQh0AHb47ne/66g7
|
||||
UMzmzZvjiSeeKD0DoBiBDsAOo0aNiiuvvLL0DKCOOeYO1DOBDsA7nHHGGTF16tTSM4A61dbWFt3d
|
||||
3aVnABQh0AHYyfe///3Yd999S88A6tCaNWti+fLlpWcAFCHQAdhJS0tL/PCHP4zGRj8mgL7nmDtQ
|
||||
r7zzAmCXPvrRj8bFF19cegZQhwQ6UK8EOgC7ddlll8WUKVNKzwDqzN/+9rdYvXp16RkAfU6gA7Bb
|
||||
TU1NceONN8aIESNKTwHqzPz580tPAOhzAh2AdzV69Oi44YYboqGhofQUoI7Mmzev9ASAPifQAXhP
|
||||
U6dOjQsuuKD0DKCOdHR0xKZNm0rPAOhTAh2A9+Xyyy+PI488svQMoE5s27YtFi5cWHoGQJ8S6AC8
|
||||
L/369Ytbb701DjzwwNJTgDrhae5AvRHoALxvw4cPj1mzZsWwYcNKTwHqwPz582P79u2lZwD0GYEO
|
||||
wB456KCD4uabb46mpqbSU4Aat3Hjxnj66adLzwDoMwIdgD12zDHHxNVXX116BlAH2traSk8A6DMC
|
||||
HYC9cvbZZ8cXv/jF0jOAGifQgXoi0AHYa9dee2188pOfLD0DqGGrVq2Kzs7O0jMA+oRAB2CvNTY2
|
||||
xo033hgf/vCHS08Bapir6EC9EOgAfCBDhw6N2267LcaPH196ClCj5s2bV3oCQJ8Q6AB8YCNHjozb
|
||||
brstxowZU3oKUIOWLl0aa9euLT0DoNcJdAB6xOjRo+P222+P1tbW0lOAGlOpVFxFB+qCQAegx4wd
|
||||
OzZuu+222GeffUpPAWrM3LlzS08A6HUCHYAeNXHixPjJT34Szc3NpacANWTRokWxZcuW0jMAepVA
|
||||
B6DHHX744TFr1qwYNGhQ6SlAjdi6dWssXry49AyAXiXQAegVRx99dPz0pz+NIUOGlJ4C1AgftwbU
|
||||
OoEOQK+ZMmVKzJ49O0aMGFF6ClAD5s2bF5VKpfQMgF4j0AHoVYcddljceeedMXr06NJTgCq3bt26
|
||||
WLp0aekZAL1GoAPQ68aPHx933XVXjB07tvQUoMr5uDWglgl0APrEmDFj4q677oqJEyeWngJUMfeh
|
||||
A7VMoAPQZ1pbW+PnP/95HHLIIaWnAFWqs7Mz/vGPf5SeAdArBDoAfaqlpSXuvPPOOO6440pPAarU
|
||||
3LlzS08A6BUCHYA+19zcHLfeemvMnDmz9BSgCgl0oFYJdACKaGhoiJkzZ8b1118f/fv3Lz0HqCJP
|
||||
PfVUrF+/vvQMgB4n0AEo6owzzojZs2fHyJEjS08BqkSlUon29vbSMwB6nEAHoLgjjjgifvnLX8b4
|
||||
8eNLTwGqhGPuQC0S6ACkMHbs2Ljnnnti6tSppacAVaC9vT26urpKzwDoUQIdgDSGDRsWP/7xj+Pq
|
||||
q6+Ofv36lZ4DJLZ58+Z44oknSs8A6FECHYBUGhoa4rzzzou77747xowZU3oOkJhj7kCtEegApHT4
|
||||
4YfHr3/96/jUpz5VegqQVFtbW3R3d5eeAdBjBDoAae2zzz4xa9asuPrqq30UG7CTNWvWxPLly0vP
|
||||
AOgxAh2A1P77yPuHPvSh0nOAZBxzB2qJQAegKkyaNCnuvffeOPfcc6Ox0Y8v4G1tbW2lJwD0GO9w
|
||||
AKgaQ4YMiWuuuSbuuOOOOPDAA0vPARJYvnx5rF69uvQMgB4h0AGoOlOmTIn77rsvLr74YlfTgZg/
|
||||
f37pCQA9wrsaAKrSwIED4xvf+EbcddddcdBBB5WeAxQ0b9680hMAeoRAB6CqTZ48OX7729/GhRde
|
||||
6Go61KmOjo7YvHlz6RkAH1hDR0eHD48EoCa8+OKLMWvWrGhvby89BegDEyZMiOnTp8f06dNjwoQJ
|
||||
pecAfGACHYCaM3fu3Pif//mfWLVqVekpQA9qbGyMj3zkIzFt2rQ44YQTPCwSqDkCHYCatG3btvjd
|
||||
734Xs2fPjk2bNpWeA+yl/4vy/7tSPmrUqNKTAHqNQAegpr322mvxi1/8b3t391J1usZx+M6VQrqW
|
||||
tbSyLEgtCUtIijRNI6K/sKPaMOcdVVBBUVTUQVRSIpLlQfRGlNILaEYWug82IxMje2aY6nfbui74
|
||||
4WKh+D3TDz74/CfOnTsXi4uLRc8B/oaGhoYYGBiIkZGROHr0aLS0tBQ9CeCnEOgA1ISJiYk4ceJE
|
||||
3L9/v+gpwAqamppieHg4jh07FkNDQ7Fu3bqiJwH8dAIdgJoyNjYWp06dinv37hU9BWpec3NzDA8P
|
||||
x/Hjx+PQoUPR0NBQ9CSAQgl0AGrS2NhYnDx5MkZHR4ueAjVl27ZtMTIyEkeOHIn9+/fH2rVri54E
|
||||
kIZAB6CmCXX4serq6mL37t0xMjLiOjSAvyDQASAibt++Hb/99luMjY0VPQVWvXK5HENDQ3HkyJEY
|
||||
GhqK5ubmoicBrAoCHQD+4NGjR3HmzJm4cOFCLCwsFD0HVo329vY4dOhQDA8Px+DgYNTX1xc9CWDV
|
||||
EegAsIJ3797F+fPn4/Tp0zEzM1P0HEjnj0fXR0ZGoqenp+hJAKueQAeA/2NhYSEuXboUp0+fjqmp
|
||||
qaLnQKE2bNgQ/f39MTQ0FIcPH45qtVr0JIBfikAHgL9pdHQ0zpw5Ezdu3HD8nZpQKpWit7c3BgcH
|
||||
Y3BwMHp6eqKurq7oWQC/LIEOAP/Q7OxsXLlyJS5evBjj4+OxtORHKb+O9vb2GBgYiMHBwTh48GBU
|
||||
KpWiJwHUDIEOAP/C69ev4/Lly3H27Nl4+fJl0XPgH6tWq3HgwIHo7++Pvr4+16ABFEigA8B3sLi4
|
||||
GA8ePIgLFy7E9evXY25uruhJsKJyuRz79++PgwcPRn9/f3R1dcWaNWuKngVACHQA+O4WFxdjfHw8
|
||||
rl69GteuXYvp6emiJ1HDWlpaYu/evdHX1xf79u2L3t7eWLt2bdGzAFiBQAeAH+zJkydx9erVuHXr
|
||||
VkxOThY9h1/cxo0bo6+vb/nIemdnp7+QA6wSAh0AfqLnz5/HjRs34ubNmzExMRFfv34tehKrWKVS
|
||||
ib1790Zvb+/yR1efAaxeAh0ACjI/Px9jY2MxOjoao6Oj8ejRo1hcXCx6FknV19fHrl27vonxHTt2
|
||||
+Os4wC9EoANAEvPz8zExMRF37tyJu3fvxuPHjwV7jSqXy7Fz587o6emJrq6u6Orqij179kRDQ0PR
|
||||
0wD4gQQ6ACT1/v37GB8fj4cPH8bk5GQ8fPgwPnz4UPQsvqOGhobo6OiIjo6O2LlzZ3R3d0d3d3ds
|
||||
3bq16GkAFECgA8Aq8urVq+VYn5ycjMnJyZidnS16Fn+hUqnEjh07oqurKzo6OqKzszM6Ozujvb09
|
||||
6urqip4HQBICHQBWsaWlpXjx4kVMTU3F8+fP4+nTp/H06dN49uxZfPz4seh5NaNUKkVbW1ts3749
|
||||
tm3b9s2zffv2aG5uLnoiAKuAQAeAX9TMzEw8e/Zs+fk93Kenp+PLly9Fz1s1SqVStLa2xpYtW2Lj
|
||||
xo3R1tYWmzdvjk2bNi2/bmtrc7c4AP+aQAeAGvT27duYmZmJ6enpmJ6ejpmZmXjz5s03r+fn54ue
|
||||
+UPU19dHpVKJDRs2LD/VajWq1WqsX79++b3W1taoVqvR2trqGDoAP4VABwBWND8/H7Ozs988c3Nz
|
||||
MTc3t+L7S0tLMTc3t/z1s7OzsbT0v18zPn36tHzn++fPn+PLly9RLpdX/L7lcvlPV4f9fkS8Uqks
|
||||
f05dXV00NTVFqVSKxsbGKJVK0dTU9KenUqlEuVyOpqamKJfL/hM6AGk5iwUArKixsTEaGxujra2t
|
||||
6CkAUBOc1wIAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCA
|
||||
QAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAE
|
||||
BDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAk
|
||||
INABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAg
|
||||
AYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAA
|
||||
CQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAA
|
||||
SECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAA
|
||||
QAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAA
|
||||
ABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAA
|
||||
AJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcA
|
||||
AIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoA
|
||||
AAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINAB
|
||||
AAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEO
|
||||
AAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0
|
||||
AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECg
|
||||
AwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAIC
|
||||
HQAAABIQ6AAAAJDAfwHNjj3TR6+CggAAAABJRU5ErkJggg==
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
@@ -1,5 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
mod transport_smtp;
|
||||
mod transport_stub;
|
||||
mod transport_file;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user