Compare commits
624 Commits
v0.9.3
...
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 | ||
|
|
574afb0c9b | ||
|
|
e17c1b8754 | ||
|
|
93066e4ca0 | ||
|
|
56b718f04d | ||
|
|
629b4b0501 | ||
|
|
c638650d0a | ||
|
|
1cbcbbb11f | ||
|
|
c9bd7ed852 | ||
|
|
334ce235ff | ||
|
|
10d362f509 | ||
|
|
2e7bd5708f | ||
|
|
cfe4ebf8cb | ||
|
|
8f1c9dbec5 | ||
|
|
0c055b50d1 | ||
|
|
139c07ca10 | ||
|
|
5bb7316722 | ||
|
|
54f4cfcdab | ||
|
|
9db66ce8e8 | ||
|
|
d5e9ebc0db | ||
|
|
cabc625009 |
@@ -1,14 +0,0 @@
|
||||
environment:
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
build: false
|
||||
test_script:
|
||||
- cargo build --verbose --manifest-path lettre/Cargo.toml
|
||||
- cargo test --verbose --manifest-path lettre_email/Cargo.toml
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
|
||||
tar xzf master.tar.gz
|
||||
cd kcov-master
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install DESTDIR=../../kcov-build
|
||||
cd ../..
|
||||
rm -rf kcov-master
|
||||
for file in target/debug/lettre*[^\.d]; do
|
||||
mkdir -p "target/cov/$(basename $file)"
|
||||
./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
|
||||
done
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
echo "Uploaded code coverage"
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
cd website
|
||||
make clean && make
|
||||
echo "lettre.at" > _book/html/CNAME
|
||||
sudo pip install ghp-import
|
||||
ghp-import -n _book/html
|
||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
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
|
||||
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 }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,6 @@
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,29 +0,0 @@
|
||||
language: rust
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- 1.32.0
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- postfix
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- cmake
|
||||
- gcc
|
||||
- binutils-dev
|
||||
- libiberty-dev
|
||||
before_script:
|
||||
- smtp-sink 2525 1000&
|
||||
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
|
||||
script:
|
||||
- cargo test --verbose --all --all-features
|
||||
after_success:
|
||||
- ./.build-scripts/codecov.sh
|
||||
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
|
||||
545
CHANGELOG.md
545
CHANGELOG.md
@@ -1,3 +1,546 @@
|
||||
<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)
|
||||
|
||||
@@ -157,6 +700,6 @@
|
||||
|
||||
* multipart support
|
||||
* add non-consuming methods for Email builders
|
||||
* `add_header` does not return the builder anymore,
|
||||
* `add_header` does not return the builder anymore,
|
||||
for consistency with other methods. Use the `header`
|
||||
method instead
|
||||
|
||||
@@ -34,13 +34,13 @@ This Code of Conduct applies both within project spaces and in public spaces whe
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.at. 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.
|
||||
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 [http://contributor-covenant.org/version/1/4][version]
|
||||
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]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[version]: https://www.contributor-covenant.org/version/1/4/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
## Contributing to Lettre
|
||||
|
||||
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
The following guidelines are inspired by the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Code formatting
|
||||
|
||||
@@ -33,3 +33,11 @@ Any line of the commit message cannot be longer 72 characters.
|
||||
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
195
Cargo.toml
195
Cargo.toml
@@ -1,5 +1,190 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"lettre",
|
||||
"lettre_email",
|
||||
]
|
||||
[package]
|
||||
name = "lettre"
|
||||
# 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"
|
||||
homepage = "https://lettre.rs"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
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]
|
||||
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]
|
||||
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]
|
||||
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-2018 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
|
||||
|
||||
140
README.md
140
README.md
@@ -1,25 +1,38 @@
|
||||
# lettre
|
||||
<h1 align="center">lettre</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
A mailer library for Rust
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
**Lettre is a mailer library for Rust.**
|
||||
<br />
|
||||
|
||||
[](https://travis-ci.org/lettre/lettre)
|
||||
[](https://ci.appveyor.com/project/amousset/lettre/branch/master)
|
||||
[](https://codecov.io/gh/lettre/lettre)
|
||||
<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>
|
||||
|
||||
[](https://crates.io/crates/lettre)
|
||||
[](https://docs.rs/lettre/)
|
||||
[]()
|
||||
[](./LICENSE)
|
||||
|
||||
[](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Percentage of issues still open")
|
||||
|
||||
Useful links:
|
||||
|
||||
* [User documentation](http://lettre.at/)
|
||||
* [API documentation](https://docs.rs/lettre/)
|
||||
* [Changelog](https://github.com/lettre/lettre/blob/master/CHANGELOG.md)
|
||||
<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>
|
||||
|
||||
---
|
||||
|
||||
@@ -31,56 +44,84 @@ Lettre provides the following features:
|
||||
* 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.32 or newer.
|
||||
This library requires Rust 1.74 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
lettre = "0.11"
|
||||
```
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
|
||||
use lettre::{EmailTransport, SmtpTransport};
|
||||
use lettre_email::EmailBuilder;
|
||||
use std::path::Path;
|
||||
use lettre::message::header::ContentType;
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = EmailBuilder::new()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.build()
|
||||
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();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
|
||||
.build();
|
||||
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
|
||||
let result = mailer.send(&email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => println!("Email sent successfully!"),
|
||||
Err(e) => panic!("Could not send email: {e:?}"),
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
## 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.
|
||||
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
|
||||
|
||||
@@ -91,4 +132,7 @@ this GitHub repository, must follow our [code of conduct](https://github.com/let
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
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);
|
||||
46
benches/transport_smtp.rs
Normal file
46
benches/transport_smtp.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{message::header::ContentType, Message, SmtpTransport, Transport};
|
||||
|
||||
fn bench_simple_send(c: &mut Criterion) {
|
||||
let sender = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
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());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
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());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
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 +0,0 @@
|
||||
../CHANGELOG.md
|
||||
@@ -1,55 +0,0 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.9.1" # remember to update html_root_url
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre" }
|
||||
appveyor = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
|
||||
[dependencies]
|
||||
log = "^0.4"
|
||||
nom = { version = "^4.0", optional = true }
|
||||
bufstream = { version = "^0.1", optional = true }
|
||||
native-tls = { version = "^0.2", optional = true }
|
||||
base64 = { version = "^0.10", optional = true }
|
||||
hostname = { version = "^0.1", optional = true }
|
||||
serde = { version = "^1.0", optional = true }
|
||||
serde_json = { version = "^1.0", optional = true }
|
||||
serde_derive = { version = "^1.0", optional = true }
|
||||
failure = "^0.1"
|
||||
failure_derive = "^0.1"
|
||||
fast_chemail = "^0.9"
|
||||
r2d2 = { version = "^0.8", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "^0.6"
|
||||
glob = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["file-transport", "smtp-transport", "sendmail-transport"]
|
||||
unstable = []
|
||||
serde-impls = ["serde", "serde_derive"]
|
||||
file-transport = ["serde-impls", "serde_json"]
|
||||
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
|
||||
sendmail-transport = []
|
||||
connection-pool = [ "r2d2" ]
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_gmail"
|
||||
required-features = ["smtp-transport"]
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -1 +0,0 @@
|
||||
../README.md
|
||||
@@ -1,50 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate lettre;
|
||||
extern crate test;
|
||||
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, Envelope, SmtpTransport};
|
||||
use lettre::{EmailAddress, SendableEmail, Transport};
|
||||
|
||||
#[bench]
|
||||
fn bench_simple_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
sender.close()
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let creds = Credentials::new(
|
||||
"example_username".to_string(),
|
||||
"example_password".to_string(),
|
||||
);
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.transport();
|
||||
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
use failure;
|
||||
|
||||
/// Error type for email content
|
||||
#[derive(Fail, Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
#[fail(display = "missing source address, invalid envelope")]
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
#[fail(display = "missing destination address, invalid envelope")]
|
||||
MissingTo,
|
||||
/// Invalid email
|
||||
#[fail(display = "invalid email address")]
|
||||
InvalidEmailAddress,
|
||||
}
|
||||
|
||||
/// Email result type
|
||||
pub type EmailResult<T> = Result<T, failure::Error>;
|
||||
@@ -1,40 +0,0 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use failure;
|
||||
use serde_json;
|
||||
use std::io;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
#[fail(display = "Internal client error: {}", error)]
|
||||
Client { error: &'static str },
|
||||
/// IO error
|
||||
#[fail(display = "IO error: {}", error)]
|
||||
Io { error: io::Error },
|
||||
/// JSON serialization error
|
||||
#[fail(display = "JSON serialization error: {}", error)]
|
||||
JsonSerialization { error: serde_json::Error },
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io { error: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
Error::JsonSerialization { error: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client { error: string }
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type FileResult = Result<(), failure::Error>;
|
||||
@@ -1,60 +0,0 @@
|
||||
//! 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, or if you want to keep track of sent messages.
|
||||
//!
|
||||
|
||||
use file::error::FileResult;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use Envelope;
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
struct SerializableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for FileTransport {
|
||||
type Result = FileResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> FileResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
let envelope = email.envelope().clone();
|
||||
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.json", message_id));
|
||||
|
||||
let serialized = serde_json::to_string(&SerializableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||
})?;
|
||||
|
||||
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//!
|
||||
//! This mailer contains the available transports for your emails.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.9.1")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate base64;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate bufstream;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate hostname;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate native_tls;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
extern crate serde;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate failure;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
extern crate fast_chemail;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
extern crate r2d2;
|
||||
|
||||
pub mod error;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
use error::EmailResult;
|
||||
use error::Error as EmailError;
|
||||
use failure::Error;
|
||||
use fast_chemail::is_valid_email;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use sendmail::SendmailTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::client::net::ClientTlsParameters;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Email address
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(address: String) -> EmailResult<EmailAddress> {
|
||||
if !is_valid_email(&address) && !address.ends_with("localhost") {
|
||||
Err(EmailError::InvalidEmailAddress)?;
|
||||
}
|
||||
Ok(EmailAddress(address))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailAddress::new(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EmailAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for EmailAddress {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
&self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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-impls", derive(Serialize, Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<EmailAddress>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<EmailAddress>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
|
||||
if to.is_empty() {
|
||||
Err(EmailError::MissingTo)?;
|
||||
}
|
||||
Ok(Envelope {
|
||||
forward_path: to,
|
||||
reverse_path: from,
|
||||
})
|
||||
}
|
||||
|
||||
/// Destination addresses of the envelope
|
||||
pub fn to(&self) -> &[EmailAddress] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Source address of the envelope
|
||||
pub fn from(&self) -> Option<&EmailAddress> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Reader(Box<Read + Send>),
|
||||
Bytes(Cursor<Vec<u8>>),
|
||||
}
|
||||
|
||||
impl Read for Message {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
Message::Reader(ref mut rdr) => rdr.read(buf),
|
||||
Message::Bytes(ref mut rdr) => rdr.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendable email structure
|
||||
pub struct SendableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Message,
|
||||
}
|
||||
|
||||
impl SendableEmail {
|
||||
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Bytes(Cursor::new(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_reader(
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Box<Read + Send>,
|
||||
) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Reader(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
pub fn message_id(&self) -> &str {
|
||||
&self.message_id
|
||||
}
|
||||
|
||||
pub fn message(self) -> Message {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn message_to_string(mut self) -> Result<String, io::Error> {
|
||||
let mut message_content = String::new();
|
||||
self.message.read_to_string(&mut message_content)?;
|
||||
Ok(message_content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait Transport<'a> {
|
||||
/// Result type for the transport
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
fn send(&mut self, email: SendableEmail) -> Self::Result;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use failure;
|
||||
use std::io;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
#[fail(display = "Internal client error: {}", error)]
|
||||
Client { error: &'static str },
|
||||
/// IO error
|
||||
#[fail(display = "IO error: {}", error)]
|
||||
Io { error: io::Error },
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io { error: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client { error: string }
|
||||
}
|
||||
}
|
||||
|
||||
/// sendmail result type
|
||||
pub type SendmailResult = Result<(), failure::Error>;
|
||||
@@ -1,80 +0,0 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
|
||||
use sendmail::error::SendmailResult;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct SendmailTransport {
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: "/usr/sbin/sendmail".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
type Result = SendmailResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> SendmailResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = Command::new(&self.command)
|
||||
.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(
|
||||
email
|
||||
.envelope()
|
||||
.from()
|
||||
.map(|x| x.as_ref())
|
||||
.unwrap_or("\"\""),
|
||||
)
|
||||
.args(email.envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())?;
|
||||
|
||||
info!("Wrote {} message to stdin", message_id);
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
// TODO display stderr
|
||||
Err(error::Error::Client {
|
||||
error: "The message could not be sent",
|
||||
})?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms on an encrypted connection
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Accepted authentication mechanisms on an unencrypted connection
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||
|
||||
/// Convertable to user credentials
|
||||
pub trait IntoCredentials {
|
||||
/// Converts to a `Credentials` struct
|
||||
fn into_credentials(self) -> Credentials;
|
||||
}
|
||||
|
||||
impl IntoCredentials for Credentials {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
let (username, password) = self;
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// LOGIN authentication mechanism
|
||||
/// Obsolete but needed for some providers (like office365)
|
||||
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
||||
Login,
|
||||
/// Non-standard XOAUTH2 mechanism
|
||||
/// 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::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports 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,
|
||||
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(Error::Client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.authentication_identity.to_string());
|
||||
}
|
||||
|
||||
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.secret.to_string());
|
||||
}
|
||||
|
||||
Err(Error::Client("Unrecognized challenge"))
|
||||
}
|
||||
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
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Credentials, Mechanism};
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
let credentials = Credentials::new("username".to_string(), "password".to_string());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"\u{0}username\u{0}password"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
||||
|
||||
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_string(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::io::{self, Cursor, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockStream {
|
||||
reader: Arc<Mutex<MockCursor>>,
|
||||
writer: Arc<Mutex<MockCursor>>,
|
||||
}
|
||||
|
||||
impl Default for MockStream {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
pub fn new() -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_vec(vec: Vec<u8>) -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_vec(&mut self) -> Vec<u8> {
|
||||
let mut cursor = self.writer.lock().unwrap();
|
||||
let vec = cursor.get_ref().to_vec();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn next_vec(&mut self, vec: &[u8]) {
|
||||
let mut cursor = self.reader.lock().unwrap();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
cursor.get_mut().extend_from_slice(vec);
|
||||
}
|
||||
|
||||
pub fn swap(&mut self) {
|
||||
let mut cur_write = self.writer.lock().unwrap();
|
||||
let mut cur_read = self.reader.lock().unwrap();
|
||||
let vec_write = cur_write.get_ref().to_vec();
|
||||
let vec_read = cur_read.get_ref().to_vec();
|
||||
cur_write.set_position(0);
|
||||
cur_read.set_position(0);
|
||||
cur_write.get_mut().clear();
|
||||
cur_read.get_mut().clear();
|
||||
// swap cursors
|
||||
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
|
||||
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for MockStream {
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
self.writer.lock().unwrap().write(msg)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MockStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.reader.lock().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::MockStream;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn write_take_test() {
|
||||
let mut mock = MockStream::new();
|
||||
// write to mock stream
|
||||
mock.write(&[1, 2, 3]).unwrap();
|
||||
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_with_vec_test() {
|
||||
let mut mock = MockStream::with_vec(vec![4, 5]);
|
||||
let mut vec = Vec::new();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut cloned = mock.clone();
|
||||
mock.write(&[6, 7]).unwrap();
|
||||
assert_eq!(cloned.take_vec(), vec![6, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut vec = Vec::new();
|
||||
mock.write(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
}
|
||||
}
|
||||
@@ -1,312 +0,0 @@
|
||||
//! SMTP client
|
||||
|
||||
use bufstream::BufStream;
|
||||
use nom::ErrorKind as NomErrorKind;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod mock;
|
||||
pub mod net;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ClientCodec {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Adds transparency
|
||||
/// TODO: replace CR and LF by CRLF
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.write_all(b"\r\n.\r\n")?,
|
||||
1 => buf.write_all(b"\n.\r\n")?,
|
||||
2 => buf.write_all(b".\r\n")?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.escape_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
let mut start = 0;
|
||||
for (idx, byte) in frame.iter().enumerate() {
|
||||
match self.escape_count {
|
||||
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
|
||||
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
|
||||
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.escape_count == 3 {
|
||||
self.escape_count = 0;
|
||||
buf.write_all(&frame[start..idx])?;
|
||||
buf.write_all(b".")?;
|
||||
start = idx;
|
||||
}
|
||||
}
|
||||
buf.write_all(&frame[start..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InnerClient<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))
|
||||
})
|
||||
);
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
|
||||
impl<S: Write + Read> InnerClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> InnerClient<S> {
|
||||
InnerClient { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.command(QuitCommand);
|
||||
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, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.stream {
|
||||
Some(ref stream) => stream.get_ref().is_encrypted(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => {
|
||||
stream.get_mut().set_read_timeout(duration)?;
|
||||
stream.get_mut().set_write_timeout(duration)?;
|
||||
Ok(())
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
addr: &A,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> SmtpResult {
|
||||
// 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 = addr.to_socket_addrs()?;
|
||||
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
debug!("connecting to {}", server_addr);
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
|
||||
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
||||
// TODO
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = self.command(AuthCommand::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: Box<Read>) -> SmtpResult {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
|
||||
let mut message_reader = BufReader::new(message);
|
||||
|
||||
loop {
|
||||
out_buf.clear();
|
||||
|
||||
let consumed = match message_reader.fill_buf() {
|
||||
Ok(bytes) => {
|
||||
codec.encode(bytes, &mut out_buf)?;
|
||||
bytes.len()
|
||||
}
|
||||
Err(ref err) => panic!("Failed with: {}", err),
|
||||
};
|
||||
message_reader.consume(consumed);
|
||||
|
||||
if consumed == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
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) -> SmtpResult {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
self.stream.as_mut().unwrap().write_all(string)?;
|
||||
self.stream.as_mut().unwrap().flush()?;
|
||||
|
||||
debug!(
|
||||
"Wrote: {}",
|
||||
escape_crlf(String::from_utf8_lossy(string).as_ref())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn read_response(&mut self) -> SmtpResult {
|
||||
let mut raw_response = String::new();
|
||||
let mut response = raw_response.parse::<Response>();
|
||||
|
||||
while response.is_err() {
|
||||
if response.as_ref().err().unwrap() != &NomErrorKind::Complete {
|
||||
break;
|
||||
}
|
||||
// TODO read more than one line
|
||||
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
||||
|
||||
// EOF is reached
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
response = raw_response.parse::<Response>();
|
||||
}
|
||||
|
||||
debug!("Read: {}", escape_crlf(raw_response.as_ref()));
|
||||
|
||||
let final_response = response?;
|
||||
|
||||
if final_response.is_positive() {
|
||||
Ok(final_response)
|
||||
} else {
|
||||
Err(From::from(final_response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_crlf, ClientCodec};
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
|
||||
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test.", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
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,172 +0,0 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use native_tls::{Protocol, TlsConnector, TlsStream};
|
||||
use smtp::client::mock::MockStream;
|
||||
use std::io::{self, ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ClientTlsParameters {
|
||||
/// A connector from `native-tls`
|
||||
pub connector: TlsConnector,
|
||||
/// The domain to send during the TLS handshake
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl ClientTlsParameters {
|
||||
/// Creates a `ClientTlsParameters`
|
||||
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
|
||||
ClientTlsParameters { connector, domain }
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Represents the different types of underlying network streams
|
||||
pub enum NetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
Tls(TlsStream<TcpStream>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
|
||||
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.read(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.write(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
NetworkStream::Tls(ref mut s) => s.flush(),
|
||||
NetworkStream::Mock(ref mut s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, tls_parameters: Option<&ClientTlsParameters>)
|
||||
-> io::Result<Self>;
|
||||
/// Upgrades to TLS connection
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
|
||||
/// Is the NetworkStream encrypted
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(
|
||||
addr: &SocketAddr,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> io::Result<NetworkStream> {
|
||||
let tcp_stream = TcpStream::connect(addr)?;
|
||||
|
||||
match tls_parameters {
|
||||
Some(context) => context
|
||||
.connector
|
||||
.connect(context.domain.as_ref(), tcp_stream)
|
||||
.map(NetworkStream::Tls)
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e)),
|
||||
None => Ok(NetworkStream::Tcp(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
*self = match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
||||
.connector
|
||||
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
|
||||
{
|
||||
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Tls(_) => return Ok(()),
|
||||
NetworkStream::Mock(_) => return Ok(()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn is_encrypted(&self) -> bool {
|
||||
match *self {
|
||||
NetworkStream::Tcp(_) => false,
|
||||
NetworkStream::Tls(_) => true,
|
||||
NetworkStream::Mock(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for read and write timeout support
|
||||
pub trait Timeout: Sized {
|
||||
/// Set read timeout for IO calls
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl Timeout for NetworkStream {
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
|
||||
//! SMTP commands
|
||||
|
||||
use base64;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::error::Error;
|
||||
use smtp::extension::ClientId;
|
||||
use smtp::extension::{MailParameter, RcptParameter};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use EmailAddress;
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EhloCommand {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for EhloCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl EhloCommand {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
||||
EhloCommand { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct StarttlsCommand;
|
||||
|
||||
impl Display for StarttlsCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct MailCommand {
|
||||
sender: Option<EmailAddress>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for MailCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl MailCommand {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RcptCommand {
|
||||
recipient: EmailAddress,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for RcptCommand {
|
||||
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 RcptCommand {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct DataCommand;
|
||||
|
||||
impl Display for DataCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct QuitCommand;
|
||||
|
||||
impl Display for QuitCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct NoopCommand;
|
||||
|
||||
impl Display for NoopCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct HelpCommand {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for HelpCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if self.argument.is_some() {
|
||||
write!(f, " {}", self.argument.as_ref().unwrap())?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl HelpCommand {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> HelpCommand {
|
||||
HelpCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct VrfyCommand {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for VrfyCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl VrfyCommand {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> VrfyCommand {
|
||||
VrfyCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ExpnCommand {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for ExpnCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExpnCommand {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> ExpnCommand {
|
||||
ExpnCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RsetCommand;
|
||||
|
||||
impl Display for RsetCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct AuthCommand {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for AuthCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = self
|
||||
.response
|
||||
.as_ref()
|
||||
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
|
||||
|
||||
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 AuthCommand {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(AuthCommand {
|
||||
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<AuthCommand, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(AuthCommand {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use smtp::extension::MailBodyParameter;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::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!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", VrfyCommand::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", ExpnCommand::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use base64::DecodeError;
|
||||
use native_tls;
|
||||
use nom;
|
||||
use smtp::response::{Response, Severity};
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
/// 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)
|
||||
Transient(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsing(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsing(DecodeError),
|
||||
/// Error parsing UTF8in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
Resolution,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::ErrorKind),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed transient error during SMTP transaction",
|
||||
},
|
||||
Permanent(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed permanent error during SMTP transaction",
|
||||
},
|
||||
ResponseParsing(err) => err,
|
||||
ChallengeParsing(ref err) => err.description(),
|
||||
Utf8Parsing(ref err) => err.description(),
|
||||
Resolution => "could not resolve hostname",
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
Tls(ref err) => err.description(),
|
||||
Parsing(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
Io(ref err) => Some(&*err),
|
||||
Tls(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::ErrorKind> for Error {
|
||||
fn from(err: nom::ErrorKind) -> Error {
|
||||
Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Error {
|
||||
ChallengeParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
Severity::TransientNegativeCompletion => Transient(response),
|
||||
Severity::PermanentNegativeCompletion => Permanent(response),
|
||||
_ => Client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
@@ -1,479 +0,0 @@
|
||||
//! 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](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
use smtp::authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
};
|
||||
use smtp::client::net::ClientTlsParameters;
|
||||
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
||||
use smtp::client::InnerClient;
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use {SendableEmail, Transport};
|
||||
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
pub mod r2d2;
|
||||
pub mod response;
|
||||
pub mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
/// Default submission port
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub enum ClientSecurity {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
Opportunistic(ClientTlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
Required(ClientTlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
Wrapper(ClientTlsParameters),
|
||||
}
|
||||
|
||||
/// Configures connection reuse behavior
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum ConnectionReuseParameters {
|
||||
/// Unlimited connection reuse
|
||||
ReuseUnlimited,
|
||||
/// Maximum number of connection reuse
|
||||
ReuseLimited(u16),
|
||||
/// Disable connection reuse, close connection after each transaction
|
||||
NoReuse,
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
/// Enable connection reuse
|
||||
connection_reuse: ConnectionReuseParameters,
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Credentials
|
||||
credentials: Option<Credentials>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// TLS security configuration
|
||||
security: ClientSecurity,
|
||||
/// Enable UTF8 mailboxes in envelope or headers
|
||||
smtp_utf8: bool,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication_mechanism: Option<Mechanism>,
|
||||
/// 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 SmtpClient {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No connection reuse
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
///
|
||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpClient {
|
||||
server_addr: addr,
|
||||
security,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
hello_name: ClientId::hostname(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
}),
|
||||
None => Err(Error::Resolution),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple and secure transport, should be used when possible.
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
|
||||
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
||||
|
||||
SmtpClient::new(
|
||||
(domain, SUBMISSIONS_PORT),
|
||||
ClientSecurity::Wrapper(tls_parameters),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
|
||||
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
|
||||
}
|
||||
|
||||
/// Enable SMTPUTF8 if the server supports it
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
|
||||
self.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
||||
self.credentials = Some(credentials.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn transport(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
|
||||
#[allow(missing_debug_implementations)]
|
||||
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: SmtpClient,
|
||||
/// Low level client
|
||||
client: InnerClient,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.close();
|
||||
}
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl<'a> SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
||||
let client = InnerClient::new();
|
||||
|
||||
SmtpTransport {
|
||||
client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self) -> Result<(), Error> {
|
||||
// Check if the connection is still available
|
||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
||||
self.close();
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
info!(
|
||||
"connection already established to {}",
|
||||
self.client_info.server_addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.client.connect(
|
||||
&self.client_info.server_addr,
|
||||
match self.client_info.security {
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
},
|
||||
)?;
|
||||
|
||||
self.client.set_timeout(self.client_info.timeout)?;
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
self.ehlo()?;
|
||||
|
||||
match (
|
||||
&self.client_info.security.clone(),
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::StartTls),
|
||||
) {
|
||||
(&ClientSecurity::Required(_), false) => {
|
||||
return Err(From::from("Could not encrypt connection, aborting"));
|
||||
}
|
||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
||||
(&ClientSecurity::None, _) => (),
|
||||
(&ClientSecurity::Wrapper(_), _) => (),
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
||||
|
||||
debug!("connection encrypted");
|
||||
|
||||
// Send EHLO again
|
||||
self.ehlo()?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.client_info.credentials.is_some() {
|
||||
let mut found = false;
|
||||
|
||||
// Compute accepted mechanism
|
||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
||||
Some(mechanism) => vec![mechanism],
|
||||
None => {
|
||||
if self.client.is_encrypted() {
|
||||
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
|
||||
} else {
|
||||
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_auth_mechanism(mechanism)
|
||||
{
|
||||
found = true;
|
||||
try_smtp!(
|
||||
self.client
|
||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
||||
self
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms available");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
fn ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(
|
||||
self.client.command(EhloCommand::new(ClientId::new(
|
||||
self.client_info.hello_name.to_string()
|
||||
),)),
|
||||
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)
|
||||
}
|
||||
|
||||
/// Reset the client state
|
||||
pub fn close(&mut self) {
|
||||
// Close the SMTP transaction if needed
|
||||
self.client.close();
|
||||
|
||||
// Reset the client state
|
||||
self.server_info = None;
|
||||
self.state.panic = false;
|
||||
self.state.connection_reuse_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
#[cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||
)]
|
||||
fn send(&mut self, email: SendableEmail) -> SmtpResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime)
|
||||
{
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::SmtpUtfEight)
|
||||
&& self.client_info.smtp_utf8
|
||||
{
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.client.command(MailCommand::new(
|
||||
email.envelope().from().cloned(),
|
||||
mail_options,
|
||||
)),
|
||||
self
|
||||
);
|
||||
|
||||
// Log the mail command
|
||||
info!(
|
||||
"{}: from=<{}>",
|
||||
message_id,
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in email.envelope().to() {
|
||||
try_smtp!(
|
||||
self.client
|
||||
.command(RcptCommand::new(to_address.clone(), vec![])),
|
||||
self
|
||||
);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(Box::new(email.message()));
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!(
|
||||
"{}: conn_use={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
match self.client_info.connection_reuse {
|
||||
ConnectionReuseParameters::ReuseLimited(limit)
|
||||
if self.state.connection_reuse_count >= limit =>
|
||||
{
|
||||
self.close()
|
||||
}
|
||||
ConnectionReuseParameters::NoReuse => self.close(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
use r2d2::ManageConnection;
|
||||
use smtp::error::Error;
|
||||
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||
|
||||
pub struct SmtpConnectionManager {
|
||||
transport_builder: SmtpClient,
|
||||
}
|
||||
|
||||
impl SmtpConnectionManager {
|
||||
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
|
||||
Ok(SmtpConnectionManager {
|
||||
transport_builder: transport_builder
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpConnectionManager {
|
||||
type Connection = SmtpTransport;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
let mut transport = SmtpTransport::new(self.transport_builder.clone());
|
||||
transport.connect()?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.client.is_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.state.panic
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubTransport {
|
||||
response: StubResult,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given response
|
||||
pub fn new(response: StubResult) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_positive() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), ()>;
|
||||
|
||||
impl<'a> Transport<'a> for StubTransport {
|
||||
type Result = StubResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> StubResult {
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
email.envelope().to()
|
||||
);
|
||||
self.response
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||
mod test {
|
||||
extern crate lettre;
|
||||
extern crate r2d2;
|
||||
|
||||
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
|
||||
use self::lettre::{SmtpConnectionManager, Transport};
|
||||
use self::r2d2::Pool;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn email(message: &str) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
message.to_string().into_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_one() {
|
||||
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(1).build(manager).unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
let result = (*mailer).send(email("send one"));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_from_thread() {
|
||||
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(2).build(manager).unwrap();
|
||||
|
||||
let (s1, r1) = mpsc::channel();
|
||||
let (s2, r2) = mpsc::channel();
|
||||
|
||||
let pool1 = pool.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
let mut conn = pool1.get().unwrap();
|
||||
s1.send(()).unwrap();
|
||||
r2.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 1"))
|
||||
.expect("Send failed from thread 1");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
let pool2 = pool.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
let mut conn = pool2.get().unwrap();
|
||||
s2.send(()).unwrap();
|
||||
r1.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 2"))
|
||||
.expect("Send failed from thread 2");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
(*mailer)
|
||||
.send(email("send from main thread"))
|
||||
.expect("Send failed from main thread");
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_readme() {
|
||||
let readme = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("README.md");
|
||||
|
||||
skeptic_test(&readme);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/sending-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}", path)
|
||||
);
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use std::env::temp_dir;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
|
||||
);
|
||||
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
.send(email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender_ok = StubTransport::new_positive();
|
||||
let mut sender_ko = StubTransport::new(Err(()));
|
||||
let email_ok = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email_ko = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
sender_ok.send(email_ok).unwrap();
|
||||
sender_ko.send(email_ko).unwrap_err();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../CHANGELOG.md
|
||||
@@ -1,33 +0,0 @@
|
||||
[package]
|
||||
|
||||
name = "lettre_email"
|
||||
version = "0.9.1" # remember to update html_root_url
|
||||
description = "Email builder"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre_email" }
|
||||
appveyor = { repository = "lettre/lettre_email" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
|
||||
|
||||
[dev-dependencies]
|
||||
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
|
||||
glob = "0.3"
|
||||
|
||||
[dependencies]
|
||||
email = "^0.0.20"
|
||||
mime = "^0.3"
|
||||
time = "^0.1"
|
||||
uuid = { version = "^0.7", features = ["v4"] }
|
||||
lettre = { version = "^0.9", path = "../lettre", default-features = false }
|
||||
base64 = "^0.10"
|
||||
failure = "^0.1"
|
||||
failure_derive = "^0.1"
|
||||
@@ -1 +0,0 @@
|
||||
../LICENSE
|
||||
@@ -1 +0,0 @@
|
||||
../README.md
|
||||
@@ -1,34 +0,0 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate mime;
|
||||
|
||||
use lettre::{SmtpClient, Transport};
|
||||
use lettre_email::Email;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email.into());
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
use lettre;
|
||||
use std::io;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum Error {
|
||||
/// Envelope error
|
||||
#[fail(display = "lettre error: {}", error)]
|
||||
Envelope {
|
||||
/// inner error
|
||||
error: lettre::error::Error,
|
||||
},
|
||||
/// Unparseable filename for attachment
|
||||
#[fail(display = "the attachment filename could not be parsed")]
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
#[fail(display = "IO error: {}", error)]
|
||||
Io {
|
||||
/// inner error
|
||||
error: io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io { error: err }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(err: lettre::error::Error) -> Error {
|
||||
Error::Envelope { error: err }
|
||||
}
|
||||
}
|
||||
@@ -1,606 +0,0 @@
|
||||
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.1")]
|
||||
#![deny(
|
||||
missing_docs,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
|
||||
extern crate failure;
|
||||
#[macro_use]
|
||||
extern crate failure_derive;
|
||||
|
||||
extern crate base64;
|
||||
extern crate email as email_format;
|
||||
extern crate lettre;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
pub extern crate mime;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
|
||||
use error::Error as EmailError;
|
||||
use failure::Error;
|
||||
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
|
||||
use mime::Mime;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use time::{now, Tm};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Builds a `MimeMessage` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct PartBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
}
|
||||
|
||||
impl Default for PartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a message id
|
||||
pub type MessageId = String;
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: PartBuilder,
|
||||
/// The recipients' addresses for the mail header
|
||||
to: Vec<Address>,
|
||||
/// The sender addresses for the mail header
|
||||
from: Vec<Address>,
|
||||
/// The Cc addresses for the mail header
|
||||
cc: Vec<Address>,
|
||||
/// The Bcc addresses for the mail header
|
||||
bcc: Vec<Address>,
|
||||
/// The Reply-To addresses for the mail header
|
||||
reply_to: Vec<Address>,
|
||||
/// The In-Reply-To ids for the mail header
|
||||
in_reply_to: Vec<MessageId>,
|
||||
/// The References ids for the mail header
|
||||
references: Vec<MessageId>,
|
||||
/// The sender address for the mail header
|
||||
sender: Option<Mailbox>,
|
||||
/// The envelope
|
||||
envelope: Option<Envelope>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: Vec<u8>,
|
||||
/// Envelope
|
||||
envelope: Envelope,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
|
||||
impl Into<SendableEmail> for Email {
|
||||
fn into(self) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
self.envelope.clone(),
|
||||
self.message_id.to_string(),
|
||||
self.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Email {
|
||||
/// TODO
|
||||
pub fn builder() -> EmailBuilder {
|
||||
EmailBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
|
||||
self.message.headers.insert(header.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
|
||||
self.message.body = body.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
|
||||
self.message.message_type = Some(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
|
||||
self.header(("Content-Type", format!("{}", content_type).as_ref()))
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
|
||||
self.message.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets built `MimeMessage`
|
||||
pub fn build(mut self) -> MimeMessage {
|
||||
self.message.update_headers();
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: PartBuilder::new(),
|
||||
to: vec![],
|
||||
from: vec![],
|
||||
cc: vec![],
|
||||
bcc: vec![],
|
||||
reply_to: vec![],
|
||||
in_reply_to: vec![],
|
||||
references: vec![],
|
||||
sender: None,
|
||||
envelope: None,
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
|
||||
self.message = self.message.body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
|
||||
self.message = self.message.header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.from.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.cc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Bcc` header and stores the recipient address
|
||||
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.bcc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.reply_to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `In-Reply-To` header
|
||||
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.in_reply_to.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `References` header
|
||||
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.references.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.sender = Some(mailbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
|
||||
self.message = self.message.header(("Subject".to_string(), subject.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a file
|
||||
///
|
||||
/// If not specified, the filename will be extracted from the file path.
|
||||
pub fn attachment_from_file(
|
||||
self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
self.attachment(
|
||||
fs::read(path)?.as_slice(),
|
||||
filename.unwrap_or(
|
||||
path.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
.ok_or(EmailError::CannotParseFilename)?,
|
||||
),
|
||||
content_type,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a vector of bytes.
|
||||
pub fn attachment(
|
||||
self,
|
||||
body: &[u8],
|
||||
filename: &str,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
let encoded_body = base64::encode(&body);
|
||||
let content = PartBuilder::new()
|
||||
.body(encoded_body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
))
|
||||
.header(("Content-Type", content_type.to_string()))
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.build();
|
||||
|
||||
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
|
||||
}
|
||||
|
||||
/// Set the message type
|
||||
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
|
||||
self.message = self.message.message_type(message_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
|
||||
self.message = self.message.child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body)
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
|
||||
))
|
||||
.build();
|
||||
self.child(text)
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let html = PartBuilder::new()
|
||||
.body(body)
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
|
||||
))
|
||||
.build();
|
||||
self.child(html)
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(
|
||||
self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}", mime::TEXT_PLAIN_UTF_8).as_ref(),
|
||||
))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header((
|
||||
"Content-Type",
|
||||
format!("{}", mime::TEXT_HTML_UTF_8).as_ref(),
|
||||
))
|
||||
.build();
|
||||
|
||||
let alternate = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
self.message_type(MimeMultipartType::Mixed)
|
||||
.child(alternate.build())
|
||||
}
|
||||
|
||||
/// Sets the envelope for manual destination control
|
||||
/// If this function is not called, the envelope will be calculated
|
||||
/// from the "to" and "cc" addresses you set.
|
||||
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the Email
|
||||
pub fn build(mut self) -> Result<Email, Error> {
|
||||
// If there are multiple addresses in "From", the "Sender" is required.
|
||||
if self.from.len() >= 2 && self.sender.is_none() {
|
||||
// So, we must find something to put as Sender.
|
||||
for possible_sender in &self.from {
|
||||
// Only a mailbox can be used as sender, not Address::Group.
|
||||
if let Address::Mailbox(ref mbx) = *possible_sender {
|
||||
self.sender = Some(mbx.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Address::Group is not yet supported, so the line below will never panic.
|
||||
// If groups are supported one day, add another Error for this case
|
||||
// and return it here, if sender_header is still None at this point.
|
||||
assert!(self.sender.is_some());
|
||||
}
|
||||
// Add the sender header, if any.
|
||||
if let Some(ref v) = self.sender {
|
||||
self.message = self.message.header(("Sender", v.to_string().as_ref()));
|
||||
}
|
||||
// Calculate the envelope
|
||||
let envelope = match self.envelope {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
// we need to generate the envelope
|
||||
let mut to = vec![];
|
||||
// add all receivers in to_header and cc_header
|
||||
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
|
||||
match *receiver {
|
||||
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
|
||||
Address::Group(_, ref ms) => {
|
||||
for m in ms.iter() {
|
||||
to.push(EmailAddress::from_str(&m.address.clone())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let from = Some(EmailAddress::from_str(&match self.sender {
|
||||
Some(x) => Ok(x.address.clone()), // if we have a sender_header, use it
|
||||
None => {
|
||||
// use a from header
|
||||
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
|
||||
match self.from.first() {
|
||||
Some(a) => match *a {
|
||||
// if we have a from header
|
||||
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
|
||||
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
|
||||
// if it's an author group, use the first author
|
||||
Some(mailbox) => Ok(mailbox.address.clone()),
|
||||
// for an empty author group (the rarest of the rare cases)
|
||||
None => Err(EmailError::Envelope {
|
||||
error: LettreError::MissingFrom,
|
||||
}), // empty envelope sender
|
||||
},
|
||||
},
|
||||
// if we don't have a from header
|
||||
None => Err(EmailError::Envelope {
|
||||
error: LettreError::MissingFrom,
|
||||
}), // empty envelope sender
|
||||
}
|
||||
}
|
||||
}?)?);
|
||||
Envelope::new(from, to)?
|
||||
}
|
||||
};
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
if !self.to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("To".into(), self.to).unwrap());
|
||||
}
|
||||
if !self.from.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), self.from).unwrap());
|
||||
} else if let Some(from) = envelope.from() {
|
||||
let from = vec![Address::new_mailbox(from.to_string())];
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), from).unwrap());
|
||||
} else {
|
||||
Err(EmailError::Envelope {
|
||||
error: LettreError::MissingFrom,
|
||||
})?;
|
||||
}
|
||||
if !self.cc.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
|
||||
}
|
||||
if !self.reply_to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
|
||||
}
|
||||
if !self.in_reply_to.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.message = self
|
||||
.message
|
||||
.header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||
}
|
||||
|
||||
self.message = self.message.header(("MIME-Version", "1.0"));
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
if let Ok(header) = Header::new_with_value(
|
||||
"Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id),
|
||||
) {
|
||||
self.message = self.message.header(header)
|
||||
}
|
||||
|
||||
Ok(Email {
|
||||
message: self.message.build().as_string().into_bytes(),
|
||||
envelope,
|
||||
message_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{EmailBuilder, SendableEmail};
|
||||
use lettre::EmailAddress;
|
||||
use time::now;
|
||||
|
||||
#[test]
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
let email: SendableEmail = email_builder
|
||||
.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
.subject("Invitation")
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
|
||||
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.in_reply_to("original".to_string())
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
|
||||
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
|
||||
MIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
assert_eq!(
|
||||
email.envelope().from().unwrap().to_string(),
|
||||
"sender@localhost".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
email.envelope().to(),
|
||||
vec![
|
||||
EmailAddress::new("user@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("cc@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
|
||||
]
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
use lettre::{EmailAddress, Envelope};
|
||||
use lettre_email::EmailBuilder;
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_test() {
|
||||
let e = Envelope::new(
|
||||
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_without_from_test() {
|
||||
let e = Envelope::new(
|
||||
None,
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap_err();
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/creating-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}!", path)
|
||||
);
|
||||
}
|
||||
3
rustfmt.toml
Normal file
3
rustfmt.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
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)
|
||||
}
|
||||
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 {}
|
||||
}
|
||||
384
src/lib.rs
Normal file
384
src/lib.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
//! Lettre is an email library that allows creating and sending messages. It provides:
|
||||
//!
|
||||
//! * An easy to use email builder
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//! * Async support
|
||||
//!
|
||||
//! Lettre requires Rust 1.74 or newer.
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
//! This section lists each lettre feature and briefly explains it.
|
||||
//! More info about each module can be found in the corresponding module page.
|
||||
//!
|
||||
//! Features with `📫` near them are enabled by default.
|
||||
//!
|
||||
//! ### Typed message builder
|
||||
//!
|
||||
//! _Strongly typed [`message`] builder_
|
||||
//!
|
||||
//! * **builder** 📫: Enable the [`Message`] builder
|
||||
//! * **hostname** 📫: Try to use the actual system hostname in the `Message-ID` header
|
||||
//!
|
||||
//! ### SMTP transport
|
||||
//!
|
||||
//! _Send emails using [`SMTP`]_
|
||||
//!
|
||||
//! * **smtp-transport** 📫: Enable the SMTP transport
|
||||
//! * **pool** 📫: Connection pool for SMTP transport
|
||||
//! * **hostname** 📫: Try to use the actual system hostname for the SMTP `CLIENTID`
|
||||
//!
|
||||
//! #### SMTP over TLS via the native-tls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `native-tls` crate_
|
||||
//!
|
||||
//! Uses schannel on Windows, Security-Framework on macOS, and OpenSSL
|
||||
//! on all other platforms.
|
||||
//!
|
||||
//! * **native-tls** 📫: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-native-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: native-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! ##### Building lettre with OpenSSL
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! | Distro | Build-time packages | Runtime packages |
|
||||
//! | ------------ | -------------------------- | ---------------------------- |
|
||||
//! | Debian | `pkg-config`, `libssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//! | Alpine Linux | `pkgconf`, `openssl-dev` | `libssl3`, `ca-certificates` |
|
||||
//!
|
||||
//! #### SMTP over TLS via the boring crate (Boring TLS)
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `boring-tls` crate_
|
||||
//!
|
||||
//! * **boring-tls**: TLS support for the synchronous version of the API
|
||||
//! * **tokio1-boring-tls**: TLS support for the `tokio1` async version of the API
|
||||
//!
|
||||
//! NOTE: boring-tls isn't supported with `async-std`
|
||||
//!
|
||||
//! #### SMTP over TLS via the rustls crate
|
||||
//!
|
||||
//! _Secure SMTP connections using TLS from the `rustls` crate_
|
||||
//!
|
||||
//! * **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
|
||||
//!
|
||||
//! ##### rustls crypto backends
|
||||
//!
|
||||
//! _The crypto implementation to use with rustls_
|
||||
//!
|
||||
//! When the `rustls` feature is enabled, one of the following crypto backends MUST also
|
||||
//! be enabled.
|
||||
//!
|
||||
//! * **aws-lc-rs**: use [AWS-LC] (via [`aws-lc-rs`]) as the `rustls` crypto backend
|
||||
//! * **ring**: use [`ring`] as the `rustls` crypto backend
|
||||
//!
|
||||
//! When enabling `aws-lc-rs`, the `fips` feature can also be enabled to have
|
||||
//! rustls use the FIPS certified module of AWS-LC.
|
||||
//!
|
||||
//! `aws-lc-rs` may require cmake on some platforms to compile.
|
||||
//! `fips` always requires cmake and the Go compiler to compile.
|
||||
//!
|
||||
//! ##### rustls certificate verification backend
|
||||
//!
|
||||
//! _The TLS certificate verification backend to use with rustls_
|
||||
//!
|
||||
//! When the `rustls` feature is enabled, one of the following verification backends
|
||||
//! MUST also be enabled.
|
||||
//!
|
||||
//! * **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`])
|
||||
//!
|
||||
//! The following packages will need to be installed in order for the build
|
||||
//! stage and the compiled program to run properly.
|
||||
//!
|
||||
//! | 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 |
|
||||
//!
|
||||
//! ### 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))]
|
||||
|
||||
#[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"
|
||||
);
|
||||
|
||||
#[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;
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user