Compare commits
778 Commits
v0.3.x
...
v0.10.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed50ea74ba | ||
|
|
20d0f8f3ba | ||
|
|
690b143ea3 | ||
|
|
7f384bc983 | ||
|
|
d8c4a66206 | ||
|
|
54cd221de7 | ||
|
|
1a0c344c91 | ||
|
|
15030fde53 | ||
|
|
89fa5cdb80 | ||
|
|
aac5c9929f | ||
|
|
210133a078 | ||
|
|
a4c0af9cf1 | ||
|
|
ad9699827e | ||
|
|
f06b8f3823 | ||
|
|
170e929a2b | ||
|
|
d3c73d8bd7 | ||
|
|
4e13963bf3 | ||
|
|
ca6399acbf | ||
|
|
3f03b6296b | ||
|
|
3683d122ba | ||
|
|
0e3526c1bc | ||
|
|
50ac1cdbec | ||
|
|
227da8ac09 | ||
|
|
86763ccefb | ||
|
|
65d952a64f | ||
|
|
2e8d43baae | ||
|
|
648cf4ce5e | ||
|
|
4a9d4fbf7e | ||
|
|
a0da9fc2b9 | ||
|
|
aa31e4fff6 | ||
|
|
b187885e70 | ||
|
|
6216fd92c8 | ||
|
|
0de49c000c | ||
|
|
b55bcfb6fb | ||
|
|
04f064ed5a | ||
|
|
13b48b656d | ||
|
|
2ac6a72a8e | ||
|
|
f1e86c809d | ||
|
|
0b8d5d20ad | ||
|
|
313eb74ea5 | ||
|
|
6afc078545 | ||
|
|
697da9f7db | ||
|
|
1ea562b15a | ||
|
|
b43c69af47 | ||
|
|
449f317246 | ||
|
|
17d644181a | ||
|
|
583df6af18 | ||
|
|
ed9ca92de8 | ||
|
|
0174a29a45 | ||
|
|
bfd3300df3 | ||
|
|
6526eff5b2 | ||
|
|
e00eff8b2a | ||
|
|
5beef57c18 | ||
|
|
b10b04ada8 | ||
|
|
30a8797acf | ||
|
|
c5fef28ac9 | ||
|
|
bf32554e51 | ||
|
|
9983bb53c3 | ||
|
|
8e49c60ff8 | ||
|
|
e156520feb | ||
|
|
ec7d63c8de | ||
|
|
e5460c4ba1 | ||
|
|
d2912a3e3f | ||
|
|
9b3bd00a61 | ||
|
|
990de687aa | ||
|
|
31a3be1cba | ||
|
|
47dfdf7ee8 | ||
|
|
47c4077b14 | ||
|
|
8869c7fdb4 | ||
|
|
3cf89935af | ||
|
|
ce957ee346 | ||
|
|
f87c80e05c | ||
|
|
1c4a3f0fb3 | ||
|
|
393d414700 | ||
|
|
a83f927109 | ||
|
|
c59f67d808 | ||
|
|
087e1e9c31 | ||
|
|
69d48c4be7 | ||
|
|
04b42879b0 | ||
|
|
d5c1ab8dd1 | ||
|
|
42a34175ac | ||
|
|
542ea4ffd2 | ||
|
|
41d68616e0 | ||
|
|
36aab20086 | ||
|
|
98f09117f7 | ||
|
|
c0ef9a38a1 | ||
|
|
6b6f130070 | ||
|
|
694a6d2852 | ||
|
|
f865fc1bce | ||
|
|
60e3a0b7cb | ||
|
|
c8ec8984b8 | ||
|
|
1b45c6dd58 | ||
|
|
470b8c3ca7 | ||
|
|
c8d73dd940 | ||
|
|
bcbdbecd95 | ||
|
|
d75fb5956b | ||
|
|
211aa389d7 | ||
|
|
49787e0c41 | ||
|
|
3e62efb46a | ||
|
|
6c440bda73 | ||
|
|
cedfd8bfbb | ||
|
|
889ef0ba6a | ||
|
|
e7f07c5ce8 | ||
|
|
4b238829c7 | ||
|
|
fbbd015109 | ||
|
|
8fa66c1e0f | ||
|
|
72015da467 | ||
|
|
4b693e2ae3 | ||
|
|
ff2baacc3d | ||
|
|
2173bc5f43 | ||
|
|
df6169bc98 | ||
|
|
598abcc589 | ||
|
|
213fe1dc4e | ||
|
|
75e309731e | ||
|
|
95bc3e6745 | ||
|
|
e2b641ae89 | ||
|
|
b3ad137691 | ||
|
|
f86c544792 | ||
|
|
f8ea0c384d | ||
|
|
9e24786f67 | ||
|
|
8a5dc32578 | ||
|
|
f4a580bb90 | ||
|
|
cbef830df9 | ||
|
|
fba900daa5 | ||
|
|
f39f0d1527 | ||
|
|
c41948ccd8 | ||
|
|
43adb0fb11 | ||
|
|
7765a97e7d | ||
|
|
03cbed9b05 | ||
|
|
427fb4e35c | ||
|
|
e87f9950af | ||
|
|
8c8aa770bf | ||
|
|
0d063873fc | ||
|
|
ce08d9e8aa | ||
|
|
83a0310c8c | ||
|
|
c43e205212 | ||
|
|
33b0a9e27d | ||
|
|
4f0ea6366c | ||
|
|
2ade0b8846 | ||
|
|
6499bfe3d4 | ||
|
|
4c7086968f | ||
|
|
6afdb0cf3a | ||
|
|
ed9f38d2c8 | ||
|
|
88df2a502d | ||
|
|
7dd392401c | ||
|
|
8425f1d7c4 | ||
|
|
70c33882cc | ||
|
|
349b349518 | ||
|
|
5acfa398f6 | ||
|
|
e609c6bb72 | ||
|
|
0b98ccad45 | ||
|
|
c352efcb86 | ||
|
|
e640b6fe19 | ||
|
|
4c2f40dcb6 | ||
|
|
18a89d4407 | ||
|
|
4115652695 | ||
|
|
b3414bd1ff | ||
|
|
0604030b91 | ||
|
|
dfbe6e9ba2 | ||
|
|
123794a00d | ||
|
|
1404cc8010 | ||
|
|
a661de16c9 | ||
|
|
0ac3438d32 | ||
|
|
7f22a98f2f | ||
|
|
4e500ded50 | ||
|
|
edc22e842f | ||
|
|
8b1399261f | ||
|
|
de277a2ee2 | ||
|
|
7d29d778ac | ||
|
|
8f31fb9804 | ||
|
|
482d74f7bc | ||
|
|
2fdafcd573 | ||
|
|
53aa5b4df6 | ||
|
|
a440ae5c79 | ||
|
|
16ac97e9d0 | ||
|
|
245c600c82 | ||
|
|
3995ea2983 | ||
|
|
8ed030a476 | ||
|
|
d1b54dc990 | ||
|
|
22eced1dbf | ||
|
|
f0fd4556b5 | ||
|
|
715c169167 | ||
|
|
c4d4413242 | ||
|
|
5667da9174 | ||
|
|
174791532d | ||
|
|
99df787e24 | ||
|
|
ce37464050 | ||
|
|
5e521b0c82 | ||
|
|
55ceb8f85d | ||
|
|
a4a3f33180 | ||
|
|
b20e7a0964 | ||
|
|
c4d91177dc | ||
|
|
f67d32ab86 | ||
|
|
21ae091f42 | ||
|
|
39a06862d4 | ||
|
|
6014f5c3f4 | ||
|
|
947af0acdd | ||
|
|
a90b548b4f | ||
|
|
b379adec28 | ||
|
|
29e4829f69 | ||
|
|
d2675fab82 | ||
|
|
aac3e00f8d | ||
|
|
0a450e64a8 | ||
|
|
536b4451d7 | ||
|
|
0f3f27fdb6 | ||
|
|
eb026838e3 | ||
|
|
14fac980ed | ||
|
|
ff6a4ff910 | ||
|
|
9b432aff7a | ||
|
|
92ee714f9a | ||
|
|
ec184ca5ee | ||
|
|
f306c14575 | ||
|
|
4dc4dd29c5 | ||
|
|
61b4087a40 | ||
|
|
2200cf407d | ||
|
|
afc11951a6 | ||
|
|
4b6ea72aac | ||
|
|
6deeb02139 | ||
|
|
97f60f111e | ||
|
|
4601e0f8c8 | ||
|
|
1c879718cf | ||
|
|
da7b701e60 | ||
|
|
fe79b27b44 | ||
|
|
bc60857ce4 | ||
|
|
e0910ad351 | ||
|
|
ff6408f099 | ||
|
|
7ba7560fbb | ||
|
|
32e2a551b0 | ||
|
|
bdd2076eec | ||
|
|
3eef024f77 | ||
|
|
d227cd4384 | ||
|
|
a4627f139a | ||
|
|
9b825de617 | ||
|
|
05735deb56 | ||
|
|
e5a1248a55 | ||
|
|
0e05e0e792 | ||
|
|
86e51813ca | ||
|
|
83a0185a83 | ||
|
|
ebfd00b146 | ||
|
|
4fbe7004cc | ||
|
|
da8bf9a040 | ||
|
|
6b6eadf134 | ||
|
|
75c640b4a9 | ||
|
|
24d694db3b | ||
|
|
eda7fc1501 | ||
|
|
5bc1cba2eb | ||
|
|
bf2adcabed | ||
|
|
e927d0b2e5 | ||
|
|
6eff9d3bee | ||
|
|
8336528f09 | ||
|
|
e86e33214f | ||
|
|
089b811bbc | ||
|
|
50d96ad8df | ||
|
|
657f2cd5ad | ||
|
|
e900b59008 | ||
|
|
0313576fe1 | ||
|
|
5f75afe05c | ||
|
|
0ead3cde09 | ||
|
|
4597884d31 | ||
|
|
2ad5e81e60 | ||
|
|
cf8f934c56 | ||
|
|
df949f837e | ||
|
|
574afb0c9b | ||
|
|
e17c1b8754 | ||
|
|
93066e4ca0 | ||
|
|
0a3d51dc25 | ||
|
|
56b718f04d | ||
|
|
4828cf4e92 | ||
|
|
c33de49fbb | ||
|
|
629b4b0501 | ||
|
|
4f470a2c3f | ||
|
|
a0c8fb947c | ||
|
|
c638650d0a | ||
|
|
1cbcbbb11f | ||
|
|
c9bd7ed852 | ||
|
|
334ce235ff | ||
|
|
10d362f509 | ||
|
|
2e7bd5708f | ||
|
|
cfe4ebf8cb | ||
|
|
8f1c9dbec5 | ||
|
|
0c055b50d1 | ||
|
|
139c07ca10 | ||
|
|
5bb7316722 | ||
|
|
54f4cfcdab | ||
|
|
9db66ce8e8 | ||
|
|
d5e9ebc0db | ||
|
|
cabc625009 | ||
|
|
101189882a | ||
|
|
be50862d55 | ||
|
|
2d4320ea45 | ||
|
|
0444e7833b | ||
|
|
6997ab7ce4 | ||
|
|
139eb9e67e | ||
|
|
917d34210d | ||
|
|
ed2730a0a7 | ||
|
|
81d174d7ed | ||
|
|
bdb4f78bd3 | ||
|
|
4a76fbb46c | ||
|
|
52535c4554 | ||
|
|
5918803778 | ||
|
|
72f3cd8f12 | ||
|
|
78ba8007cd | ||
|
|
e202eafb7f | ||
|
|
adbd50a6ce | ||
|
|
058fa694f0 | ||
|
|
f64721702f | ||
|
|
a8d8e2ac00 | ||
|
|
434654e9af | ||
|
|
4a77f587c3 | ||
|
|
c988b1760a | ||
|
|
e08d4e3ee5 | ||
|
|
fc91bb6ee8 | ||
|
|
ee31bbe9e3 | ||
|
|
0b92881b48 | ||
|
|
8bb97e62ca | ||
|
|
8ba1c3a3f7 | ||
|
|
644b1e59b0 | ||
|
|
e225afbec2 | ||
|
|
22555f0620 | ||
|
|
c09e3ff8cd | ||
|
|
c10fe3db84 | ||
|
|
9d14630552 | ||
|
|
186ad29424 | ||
|
|
dce8d53310 | ||
|
|
c9c82495ce | ||
|
|
283d45824e | ||
|
|
0ee089fc37 | ||
|
|
7f545301e1 | ||
|
|
ce932c15d6 | ||
|
|
afc23de20f | ||
|
|
c52c28ab80 | ||
|
|
4f9067f258 | ||
|
|
964b9dc00b | ||
|
|
866c804ef3 | ||
|
|
a7d35325ed | ||
|
|
319be26031 | ||
|
|
3a0b6e1a31 | ||
|
|
ed7c16452c | ||
|
|
2e2f614517 | ||
|
|
bc09aa2185 | ||
|
|
bab4519baa | ||
|
|
1f1359502e | ||
|
|
5423f53bad | ||
|
|
4b48bdbd9a | ||
|
|
31442e96d0 | ||
|
|
70720d7cdd | ||
|
|
706ed8b4fd | ||
|
|
da63de72fc | ||
|
|
d71b560077 | ||
|
|
917ecbc477 | ||
|
|
4a61357205 | ||
|
|
e7e0f3485d | ||
|
|
33d20c61d1 | ||
|
|
1baf8a9516 | ||
|
|
5bb4f4f8e7 | ||
|
|
2e56bd6a82 | ||
|
|
3159981e4a | ||
|
|
a0c95f748e | ||
|
|
f949dd53ed | ||
|
|
d990ab4de3 | ||
|
|
c489a0bdc2 | ||
|
|
9dd08ad4c2 | ||
|
|
4313207896 | ||
|
|
944a236aa7 | ||
|
|
ddd80f5dcd | ||
|
|
530b595424 | ||
|
|
f985cf7559 | ||
|
|
7b6ac2e677 | ||
|
|
6797d3d3f5 | ||
|
|
bffe2978d2 | ||
|
|
4d86840bc9 | ||
|
|
a7e5493aad | ||
|
|
d6828f5150 | ||
|
|
0b850e3b2f | ||
|
|
57be14112a | ||
|
|
928fd413a4 | ||
|
|
2d196599c7 | ||
|
|
ad2fef9bbc | ||
|
|
6577aa17d9 | ||
|
|
34fc101f31 | ||
|
|
91f0cfa27c | ||
|
|
78bd429310 | ||
|
|
54e3dd3e41 | ||
|
|
36381eb345 | ||
|
|
8d92dbb0c2 | ||
|
|
c4b1100bdb | ||
|
|
b5e2c67dbd | ||
|
|
bf3bb78534 | ||
|
|
a914d9990c | ||
|
|
a3d6722e7e | ||
|
|
fd56ec8877 | ||
|
|
81bad13175 | ||
|
|
27c8e206cf | ||
|
|
b4d03ead8c | ||
|
|
d692a9488f | ||
|
|
f3271715ec | ||
|
|
ee51cf7454 | ||
|
|
8981a7758c | ||
|
|
b91cb0770d | ||
|
|
0cff889ace | ||
|
|
d00568cbd6 | ||
|
|
32cace1252 | ||
|
|
a86cc3328e | ||
|
|
b51b2843f4 | ||
|
|
91a17ae281 | ||
|
|
bd752daf85 | ||
|
|
ed01efd890 | ||
|
|
088db45e41 | ||
|
|
955a453df9 | ||
|
|
71eda4b174 | ||
|
|
d283254b1a | ||
|
|
f3f963c6a5 | ||
|
|
bef45c48f7 | ||
|
|
e024806402 | ||
|
|
d7a8574464 | ||
|
|
9b22f5867e | ||
|
|
17abeb3957 | ||
|
|
e6a5c158da | ||
|
|
f4fc427a03 | ||
|
|
662072e692 | ||
|
|
4f16d9ee69 | ||
|
|
9d68629bb6 | ||
|
|
96e4f845ec | ||
|
|
4dc95281ad | ||
|
|
f3311456ad | ||
|
|
98a250f015 | ||
|
|
f2f2f98905 | ||
|
|
53e79d9620 | ||
|
|
4f11ae61ef | ||
|
|
9344ff7e5c | ||
|
|
36d20bc7b6 | ||
|
|
620c3e96dc | ||
|
|
7cab860cde | ||
|
|
f10e4e81d0 | ||
|
|
5face8614b | ||
|
|
480ed11785 | ||
|
|
a082da6ea4 | ||
|
|
2a847c1b3b | ||
|
|
f6c07f0720 | ||
|
|
bff687f55c | ||
|
|
66cd6fe3ac | ||
|
|
bc4714a2c8 | ||
|
|
547be305c5 | ||
|
|
ba719f7255 | ||
|
|
4005fc88bc | ||
|
|
173f8aa2dd | ||
|
|
aa9e9dd96e | ||
|
|
dd6601b9e5 | ||
|
|
78d8f9afb7 | ||
|
|
487bee0769 | ||
|
|
ab35bac204 | ||
|
|
30ea70edab | ||
|
|
eb4e7f9829 | ||
|
|
104935b443 | ||
|
|
1936211f8e | ||
|
|
87d0dbdf70 | ||
|
|
7bc28caf27 | ||
|
|
7498bed378 | ||
|
|
d2475ae1aa | ||
|
|
12174676d3 | ||
|
|
1850d56ec1 | ||
|
|
92134e22a4 | ||
|
|
01fde07a48 | ||
|
|
16223ee9c3 | ||
|
|
b010126c19 | ||
|
|
166178b011 | ||
|
|
aecbce50e3 | ||
|
|
cc324b4705 | ||
|
|
2785f14f31 | ||
|
|
0d3988d499 | ||
|
|
44a1c40d41 | ||
|
|
bef224105c | ||
|
|
776f12c99b | ||
|
|
1305277ba2 | ||
|
|
85b5bbaae0 | ||
|
|
8519f6881d | ||
|
|
60ac8ae0f3 | ||
|
|
4e9a5575a6 | ||
|
|
1f11a3ae94 | ||
|
|
9be980ce0b | ||
|
|
2838174c65 | ||
|
|
445623db30 | ||
|
|
3216a3f0b9 | ||
|
|
87e25490b2 | ||
|
|
b4250036c6 | ||
|
|
4012d58dca | ||
|
|
75c184c5c3 | ||
|
|
b087dec2d9 | ||
|
|
f07fe8687d | ||
|
|
e81351bfa8 | ||
|
|
c34f3443f5 | ||
|
|
9f4ae7b8dc | ||
|
|
8bfee207b4 | ||
|
|
9bf5adc052 | ||
|
|
a18e219000 | ||
|
|
b7e4bfb375 | ||
|
|
66836b0522 | ||
|
|
59d47dfdf5 | ||
|
|
75e6c0d115 | ||
|
|
a093e38f7e | ||
|
|
7d535f29a8 | ||
|
|
2bfd67273b | ||
|
|
e656e9e325 | ||
|
|
e90fe50943 | ||
|
|
2aa3cd0670 | ||
|
|
a2143caf02 | ||
|
|
04c83fc20d | ||
|
|
c7c42cb207 | ||
|
|
8a90f8f7b6 | ||
|
|
0e01820ea4 | ||
|
|
e2d0e31453 | ||
|
|
669c558120 | ||
|
|
12794d36b3 | ||
|
|
0d7cba9657 | ||
|
|
215c9d5136 | ||
|
|
77210d1b5e | ||
|
|
441c4e8228 | ||
|
|
858cecfdde | ||
|
|
1047e84962 | ||
|
|
7fc0ed5c56 | ||
|
|
6840555473 | ||
|
|
b31fd465ad | ||
|
|
b1d2a89b2e | ||
|
|
5b2a1be24c | ||
|
|
7e69f90a6f | ||
|
|
f95bbff997 | ||
|
|
339f65f618 | ||
|
|
1ae8c6370c | ||
|
|
eb3b88288a | ||
|
|
9b8449a908 | ||
|
|
ff87e4c595 | ||
|
|
918c679d94 | ||
|
|
04e9a824b3 | ||
|
|
63f35f78a6 | ||
|
|
1378b8959d | ||
|
|
c5034324d2 | ||
|
|
7eb0828bca | ||
|
|
80fb92161d | ||
|
|
fc741b7390 | ||
|
|
153af016e7 | ||
|
|
d3a4e353b1 | ||
|
|
4a49db92c1 | ||
|
|
eea9354e2f | ||
|
|
e6d62a5e64 | ||
|
|
782962ce5a | ||
|
|
53f1d07f00 | ||
|
|
a1de7a2b24 | ||
|
|
bae92bcf08 | ||
|
|
df91f38323 | ||
|
|
622c4a8ff0 | ||
|
|
b27013765a | ||
|
|
53072e2c89 | ||
|
|
c3a2409957 | ||
|
|
093e16cad0 | ||
|
|
28fb7961df | ||
|
|
9c3991af6d | ||
|
|
912e0579a6 | ||
|
|
0c1b440f8b | ||
|
|
3b46c56bbd | ||
|
|
bcf2110804 | ||
|
|
7270d0807f | ||
|
|
d26a771207 | ||
|
|
ebf75c2f00 | ||
|
|
30175d941d | ||
|
|
b4161afe14 | ||
|
|
6f09f1c52c | ||
|
|
aabc07b0a9 | ||
|
|
dca316deae | ||
|
|
d429763b85 | ||
|
|
bae531e264 | ||
|
|
c63eba4d1c | ||
|
|
0db90c0997 | ||
|
|
d0b038ac72 | ||
|
|
691e7ff4f8 | ||
|
|
d860051d1a | ||
|
|
a166b2e6a4 | ||
|
|
85a81ca383 | ||
|
|
e521f9bb0f | ||
|
|
d2400de365 | ||
|
|
5a76aaf839 | ||
|
|
1dddbde053 | ||
|
|
3368872f9f | ||
|
|
cb15e32454 | ||
|
|
e85c3a4d70 | ||
|
|
ea99f66a5e | ||
|
|
6757fadee3 | ||
|
|
843f6b9a39 | ||
|
|
aa7a6dfcac | ||
|
|
73c5630634 | ||
|
|
9fe4d4ad84 | ||
|
|
3b4434467a | ||
|
|
e50f287598 | ||
|
|
ae640da631 | ||
|
|
87aa3ca701 | ||
|
|
12ecad34ba | ||
|
|
11a983f078 | ||
|
|
332e05278c | ||
|
|
9942acf8ff | ||
|
|
66c214c2b7 | ||
|
|
dfa01dbb7a | ||
|
|
9e2f0af9a6 | ||
|
|
3178db04e2 | ||
|
|
4f41eef936 | ||
|
|
8069b9e9ae | ||
|
|
4d879dabba | ||
|
|
20f6c5db3f | ||
|
|
9953820174 | ||
|
|
1c8b78066f | ||
|
|
4d3e51d115 | ||
|
|
4fc73cdde0 | ||
|
|
caeb6b807c | ||
|
|
315e248d63 | ||
|
|
e068c2d41f | ||
|
|
0488e3f943 | ||
|
|
2ed25fdbb4 | ||
|
|
90b00ae4ff | ||
|
|
bd8b1265c4 | ||
|
|
f8f024ae7c | ||
|
|
0f71490c61 | ||
|
|
6fe2ef679b | ||
|
|
73e7aa3639 | ||
|
|
cc6ca7633d | ||
|
|
64d2f2e81c | ||
|
|
e572892a48 | ||
|
|
3b4f4a739e | ||
|
|
8400e47cfc | ||
|
|
b7cb4e88c4 | ||
|
|
73c957e350 | ||
|
|
783918a403 | ||
|
|
d0bf2327e3 | ||
|
|
e63730d960 | ||
|
|
d50bb404b9 | ||
|
|
747d8cabc5 | ||
|
|
b415edcfe0 | ||
|
|
0b01211a34 | ||
|
|
13ee61d5cf | ||
|
|
a302df61d4 | ||
|
|
5be0f86c83 | ||
|
|
53f9bada4c | ||
|
|
ce7d55ffa8 | ||
|
|
a6ea43a842 | ||
|
|
eac29768ae | ||
|
|
7788498762 | ||
|
|
d944aed9d3 | ||
|
|
67318ac759 | ||
|
|
90999bfc24 | ||
|
|
7635830399 | ||
|
|
4fbd06e18b | ||
|
|
5247e2c2aa | ||
|
|
8e21de8de3 | ||
|
|
d976f48b7b | ||
|
|
9ed51a2d3d | ||
|
|
ff72bcf5ef | ||
|
|
4d934685a8 | ||
|
|
a616c0d4c0 | ||
|
|
b938e2d757 | ||
|
|
b4603b4fbc | ||
|
|
495c21b776 | ||
|
|
bc874fa8f4 | ||
|
|
86c45f13c3 | ||
|
|
46ae195ba6 | ||
|
|
75a85831ab | ||
|
|
95e9f31141 | ||
|
|
79ac38c84d | ||
|
|
e2b4a964e7 | ||
|
|
f5f4c026bc | ||
|
|
efc42f5f22 | ||
|
|
d201a56571 | ||
|
|
f57279bdb7 | ||
|
|
d967bbb126 | ||
|
|
2553b32196 | ||
|
|
3f6fc56b3e | ||
|
|
97da0c0869 | ||
|
|
0acc57d36d | ||
|
|
d1bef702d6 | ||
|
|
d20d4f038b | ||
|
|
b7039a7a69 | ||
|
|
8fda23435c | ||
|
|
4f4a1436ae | ||
|
|
a8324795e5 | ||
|
|
f347dcc97b | ||
|
|
df69c8b7c2 | ||
|
|
fd20e90bb5 | ||
|
|
1da35ec17f | ||
|
|
69f29cfb45 | ||
|
|
e047aedee1 | ||
|
|
d14236be4d | ||
|
|
c814300f96 | ||
|
|
3171dec6e9 | ||
|
|
e9b658d868 | ||
|
|
72b8bc3866 | ||
|
|
f3742adc9f | ||
|
|
c5fec283f7 | ||
|
|
2d9ad22102 | ||
|
|
d0eab7a09f | ||
|
|
4f6e6185fc | ||
|
|
256624f3d8 | ||
|
|
74de004e6c | ||
|
|
b7d81016e1 | ||
|
|
6bd2b364ec | ||
|
|
bc764aec5a | ||
|
|
d18eec4d1b | ||
|
|
ec68ca2ca8 | ||
|
|
c9076fef63 | ||
|
|
c770dc6205 | ||
|
|
da8c733939 | ||
|
|
c21bdaff43 | ||
|
|
dab8b111d3 | ||
|
|
521681c0f7 | ||
|
|
2e3f82b98a | ||
|
|
2816e8ee73 | ||
|
|
4441be6b7b | ||
|
|
b992ca9694 | ||
|
|
d7e100692b | ||
|
|
2c979a6fbd | ||
|
|
f8c883f58e | ||
|
|
af20cfa8ff | ||
|
|
7a9f9111a5 | ||
|
|
89c0be219d | ||
|
|
6ee7fdb3d1 | ||
|
|
b7ac3a897f | ||
|
|
c436716277 | ||
|
|
eabdb960b0 | ||
|
|
59ba9e84dc | ||
|
|
e569c030bc | ||
|
|
72aea756fa | ||
|
|
655ae6d2ff | ||
|
|
150536d242 | ||
|
|
7d707fab25 | ||
|
|
4ec34987f8 | ||
|
|
7f3680f125 | ||
|
|
67566c2152 | ||
|
|
d863a7677e | ||
|
|
c1fe40479b | ||
|
|
8d03545062 | ||
|
|
489a6e892e | ||
|
|
b3fe1e0f65 | ||
|
|
3612ffca7a | ||
|
|
7940ad6c15 | ||
|
|
5ffb169bc9 | ||
|
|
ea0bb256cd | ||
|
|
9f177047f8 | ||
|
|
48eb859804 | ||
|
|
8d9877233d | ||
|
|
09f61a9fc9 | ||
|
|
40e749a04a | ||
|
|
4efb560bc8 | ||
|
|
500c4fb39d | ||
|
|
d488910010 | ||
|
|
4155e44dbd | ||
|
|
401118ee68 | ||
|
|
e6dd9d5a46 | ||
|
|
c8187c4a7c | ||
|
|
8f211c88a8 | ||
|
|
62df24c5b1 | ||
|
|
7ac43b73c3 | ||
|
|
3c91c065d6 | ||
|
|
9e30e7185e | ||
|
|
4da9e16bfc | ||
|
|
2977eb0509 | ||
|
|
2884da8f90 | ||
|
|
31a7504d54 | ||
|
|
9a93feea96 | ||
|
|
f102f321d3 | ||
|
|
1ba47e473c | ||
|
|
3acf21a316 | ||
|
|
544894def9 | ||
|
|
f74fb4f89c | ||
|
|
085998c730 | ||
|
|
d3d7c4b44e | ||
|
|
88b9cfb847 | ||
|
|
250ed7bcf4 | ||
|
|
63d9216df3 | ||
|
|
54758ebde9 | ||
|
|
bd67d80d3e | ||
|
|
a91db14869 | ||
|
|
b5c6663629 | ||
|
|
4b4150ed99 |
@@ -1,23 +0,0 @@
|
||||
environment:
|
||||
CARGO_TARGET: x86_64-pc-windows-gnu
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
install:
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/dist/rustc-nightly-${env:TARGET}.tar.gz"
|
||||
- ps: Start-FileDownload "https://static.rust-lang.org/cargo-dist/cargo-nightly-${env:CARGO_TARGET}.tar.gz"
|
||||
- 7z x rustc-nightly-%TARGET%.tar.gz > nul
|
||||
- 7z x rustc-nightly-%TARGET%.tar > nul
|
||||
- 7z x cargo-nightly-%CARGO_TARGET%.tar.gz > nul
|
||||
- 7z x cargo-nightly-%CARGO_TARGET%.tar > nul
|
||||
- call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" amd64
|
||||
- set PATH=%PATH%;%cd%/rustc-nightly-%TARGET%/rustc/bin
|
||||
- set PATH=%PATH%;%cd%/cargo-nightly-%CARGO_TARGET%/cargo/bin
|
||||
- SET PATH=%PATH%;C:\MinGW\bin
|
||||
- rustc -V
|
||||
- cargo -V
|
||||
|
||||
build: false
|
||||
|
||||
test_script:
|
||||
- cargo test --verbose --no-default-features
|
||||
7
.clog.toml
Normal file
7
.clog.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
[clog]
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
changelog = "CHANGELOG.md"
|
||||
|
||||
[sections]
|
||||
Style = ["style"]
|
||||
Documentation = ["docs"]
|
||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
liberapay: amousset
|
||||
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
21
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Code allowing to reproduce the bug.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- Lettre version
|
||||
- OS
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
12
.github/workflows/audit.yml
vendored
Normal file
12
.github/workflows/audit.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
name: Security audit
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
144
.github/workflows/test.yml
vendored
Normal file
144
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
|
||||
jobs:
|
||||
rustfmt:
|
||||
name: rustfmt / stable
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup update --no-self-update stable
|
||||
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@v2
|
||||
|
||||
- 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@v2
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-check
|
||||
|
||||
- name: Install rust
|
||||
run: rustup update --no-self-update stable
|
||||
|
||||
- name: Install cargo hack
|
||||
run: cargo install cargo-hack --debug
|
||||
|
||||
- name: Check with cargo hack
|
||||
run: cargo hack check --feature-powerset --depth 3
|
||||
|
||||
test:
|
||||
name: test / ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- name: stable
|
||||
rust: stable
|
||||
- name: beta
|
||||
rust: beta
|
||||
- name: 1.45.2
|
||||
rust: 1.45.2
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-test-${{ matrix.rust }}
|
||||
|
||||
- name: Install rust
|
||||
run: |
|
||||
rustup default ${{ matrix.rust }}
|
||||
rustup update --no-self-update ${{ matrix.rust }}
|
||||
|
||||
- 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: Test with no default features
|
||||
run: cargo test --no-default-features
|
||||
|
||||
- name: Test with default features
|
||||
run: cargo test
|
||||
|
||||
- name: Test with all features
|
||||
run: cargo test --all-features
|
||||
|
||||
# coverage:
|
||||
# name: Coverage
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# - uses: actions/checkout@v2
|
||||
# - 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 }}
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
.project
|
||||
/target/
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.sublime-*
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
21
.travis.yml
21
.travis.yml
@@ -1,21 +0,0 @@
|
||||
language: rust
|
||||
sudo: required
|
||||
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
before_script:
|
||||
- pip install 'travis-cargo<0.2' --user && export PATH=$HOME/.local/bin:$PATH
|
||||
script:
|
||||
- |
|
||||
travis-cargo build &&
|
||||
travis-cargo test &&
|
||||
travis-cargo doc
|
||||
after_success:
|
||||
- travis-cargo --only stable doc-upload
|
||||
- travis-cargo --only stable coveralls
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: "MaZ3TzuaAHuxmxQkfJdqRfkh7/ieScJRk0T/2yjysZhDMTYyRmp5wh/zkfW1ADuG0uc4Pqsxrsh1J9SVO7O0U5NJA8NKZi/pgiL+FHh0g4YtlHxy2xmFNB5am3Kyc+E7B4XylwTbA9S8ublVM0nvX7yX/a5fbwEUInVk2bA8fpc="
|
||||
227
CHANGELOG.md
Normal file
227
CHANGELOG.md
Normal file
@@ -0,0 +1,227 @@
|
||||
<a name="v0.10.0"></a>
|
||||
### v0.10.0 (unreleased)
|
||||
|
||||
#### Upgrade notes
|
||||
|
||||
Several breaking changes were made between 0.9 and 0.10, but changes should be straightforward:
|
||||
|
||||
* MSRV is now 1.45.2
|
||||
* 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` 0.2 and 1.0 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 `new` method of `ClientId` is deprecated
|
||||
* Rename `serde-impls` feature to `serde`
|
||||
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* Fix argument injection in `SendmailTransport` (see [RUSTSEC-2020-0069](https://github.com/RustSec/advisory-db/blob/master/crates/lettre/RUSTSEC-2020-0069.md))
|
||||
* Correctly encode header values containing non-ASCII characters
|
||||
* Timeout bug causing infinite hang
|
||||
* Fix doc tests in website
|
||||
* Fix docs for `domain` field
|
||||
|
||||
#### Misc
|
||||
|
||||
* Improve documentation, examples and tests
|
||||
* Replace `line-wrap`, `email`, `bufstream` with our own implementations
|
||||
* Remove `bytes`
|
||||
* Remove `time`
|
||||
* Remove `fast_chemail`
|
||||
* Update `base64` to 0.13
|
||||
* Update `hostname` to 0.3
|
||||
* Update to `nom` 6
|
||||
* Replace `log` with `tracing`
|
||||
* Move CI to Github Actions
|
||||
* Use criterion for benchmarks
|
||||
|
||||
<a name="v0.9.2"></a>
|
||||
### v0.9.2 (2019-06-11)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **email:**
|
||||
* Fix compilation with Rust 1.36+ ([393ef8d](https://github.com/lettre/lettre/commit/393ef8dcd1b1c6a6119d0666d5f09b12f50f6b4b))
|
||||
|
||||
<a name="v0.9.1"></a>
|
||||
### v0.9.1 (2019-05-05)
|
||||
|
||||
#### Features
|
||||
|
||||
* **email:**
|
||||
* Re-export mime crate ([a0c8fb9](https://github.com/lettre/lettre/commit/a0c8fb9))
|
||||
|
||||
<a name="v0.9.0"></a>
|
||||
### v0.9.0 (2019-03-17)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **email:**
|
||||
* Inserting 'from' from envelope into message headers ([058fa69](https://github.com/lettre/lettre/commit/058fa69))
|
||||
* Do not include Bcc addresses in headers ([ee31bbe](https://github.com/lettre/lettre/commit/ee31bbe))
|
||||
|
||||
* **transport:**
|
||||
* Write timeout is not set in smtp transport ([d71b560](https://github.com/lettre/lettre/commit/d71b560))
|
||||
* Client::read_response infinite loop ([72f3cd8](https://github.com/lettre/lettre/commit/72f3cd8))
|
||||
|
||||
#### Features
|
||||
|
||||
* **all:**
|
||||
* Update dependencies
|
||||
* Start using the failure crate for errors ([c10fe3d](https://github.com/lettre/lettre/commit/c10fe3d))
|
||||
|
||||
* **transport:**
|
||||
* Remove TLS 1.1 in accepted protocols by default (only allow TLS 1.2) ([4b48bdb](https://github.com/lettre/lettre/commit/4b48bdb))
|
||||
* Initial support for XOAUTH2 ([ed7c164](https://github.com/lettre/lettre/commit/ed7c164))
|
||||
* Remove support for CRAM-MD5 ([bc09aa2](https://github.com/lettre/lettre/commit/bc09aa2))
|
||||
* SMTP connection pool implementation with r2d2 ([434654e](https://github.com/lettre/lettre/commit/434654e))
|
||||
* Use md-5 and hmac instead of rust-crypto ([e7e0f34](https://github.com/lettre/lettre/commit/e7e0f34))
|
||||
* Gmail transport simple example ([a8d8e2a](https://github.com/lettre/lettre/commit/a8d8e2a))
|
||||
|
||||
* **email:**
|
||||
* Add In-Reply-To and References headers ([fc91bb6](https://github.com/lettre/lettre/commit/fc91bb6))
|
||||
* Remove non-chaining builder methods ([1baf8a9](https://github.com/lettre/lettre/commit/1baf8a9))
|
||||
|
||||
<a name="v0.8.2"></a>
|
||||
### v0.8.2 (2018-05-03)
|
||||
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
* **transport:** Write timeout is not set in smtp transport ([cc3580a8](https://github.com/lettre/lettre/commit/cc3580a8942e11c2addf6677f05e16fb451c7ea0))
|
||||
|
||||
#### Style
|
||||
|
||||
* **all:** Fix typos ([360c42ff](https://github.com/lettre/lettre/commit/360c42ffb8f706222eaad14e72619df1e4857814))
|
||||
|
||||
#### Features
|
||||
|
||||
* **all:**
|
||||
* Add set -xe option to build scripts ([57bbabaa](https://github.com/lettre/lettre/commit/57bbabaa6a10cc1a4de6f379e25babfee7adf6ad))
|
||||
* Move post-success scripts to separate files ([3177b58c](https://github.com/lettre/lettre/commit/3177b58c6d11ffae73c958713f6f0084173924e1))
|
||||
* Add website upload to travis build script ([a5294df6](https://github.com/lettre/lettre/commit/a5294df63728e14e24eeb851bb4403abd6a7bd36))
|
||||
* Add codecov upload in travis ([a03bfa00](https://github.com/lettre/lettre/commit/a03bfa008537b1d86ff789d0823e89ad5d99bd79))
|
||||
* Update README to put useful links at the top ([1ebbe660](https://github.com/lettre/lettre/commit/1ebbe660f5e142712f702c02d5d1e45211763b42))
|
||||
* Update badges in README and Cargo.toml ([f7ee5c42](https://github.com/lettre/lettre/commit/f7ee5c427ad71e4295f2f1d8e3e9e2dd850223e8))
|
||||
* Move docs from hugo to gitbook ([27935e32](https://github.com/lettre/lettre/commit/27935e32ef097db8db004569f35cad1d6cd30eca))
|
||||
* **transport:** Use md-5 and hmac instead of rust-crypto ([0cf018a8](https://github.com/lettre/lettre/commit/0cf018a85e4ea1ad16c7216670da560cc915ec32))
|
||||
|
||||
|
||||
|
||||
<a name="v0.8.1"></a>
|
||||
### v0.8.1 (2018-04-11)
|
||||
|
||||
#### Fix
|
||||
|
||||
* **all:**
|
||||
* Replace skeptic by some custom rustdoc invocations ([81bad131](https://github.com/lettre/lettre/commit/81bad1317519d330c46ea02f2b7a266b97cc00dd))
|
||||
|
||||
#### Documentation
|
||||
|
||||
* **all:**
|
||||
* Add changelog sections for style and docs ([b4d03ead](https://github.com/lettre/lettre/commit/b4d03ead8cce04e0c3d65a30e7a07acca9530f30))
|
||||
* Use clog to generate changelogs ([8981a775](https://github.com/lettre/lettre/commit/8981a7758c89be69974ef204c4390744aea94e4f), closes [#233](https://github.com/lettre/lettre/issues/233))
|
||||
|
||||
#### Style
|
||||
|
||||
* **transport-smtp:** Avoid useless empty format strings ([f3271715](https://github.com/lettre/lettre/commit/f3271715ecaf2793c9064462184867e4f22b0ead))
|
||||
|
||||
|
||||
|
||||
<a name="v0.8.0"></a>
|
||||
### v0.8.0 (2018-03-31)
|
||||
|
||||
#### Added
|
||||
|
||||
* Support binary files as attachment
|
||||
* Move doc to a dedicated website
|
||||
* Add tests for the doc using skeptic
|
||||
* Added a code of conduct
|
||||
* Use hostname as `ClientId` when available
|
||||
|
||||
#### Changed
|
||||
|
||||
* Detail in SMTP Response is now an enum
|
||||
* Use nom for parsing smtp responses
|
||||
* `Envelope` was moved from `lettre_email` to `lettre`
|
||||
* `EmailAddress::new()` now returns a `Result`
|
||||
* `SendableEmail` replaces `from` and `to` by `envelope` that returns an `Envelope`
|
||||
* `File` transport storage format has changed
|
||||
|
||||
#### Fixed
|
||||
|
||||
* Add missing "Bcc" headers when building the email
|
||||
* Specify utf-8 charset for html
|
||||
* Use parts for text and html methods to work with attachments
|
||||
|
||||
#### Removed
|
||||
|
||||
* `get_ehlo` and `reset` in SmtpTransport are now private
|
||||
|
||||
<a name="v0.7.0"></a>
|
||||
### v0.7.0 (2017-10-08)
|
||||
|
||||
#### Added
|
||||
|
||||
* Allow validating server certificate
|
||||
* Initial (incomplete) attachments support
|
||||
|
||||
#### Changed
|
||||
|
||||
* Split into the *lettre* and *lettre_email* crates
|
||||
* A lot of small improvements
|
||||
* Use *tls-native* instead of *openssl* in smtp transport
|
||||
|
||||
<a name="v0.6.2"></a>
|
||||
### v0.6.2 (2017-02-18)
|
||||
|
||||
#### Changed
|
||||
|
||||
* Update env-logger crate to 0.4
|
||||
* Update openssl crate to 0.9
|
||||
* Update uuid crate to 0.4
|
||||
|
||||
<a name="v0.6.1"></a>
|
||||
### v0.6.1 (2016-10-19)
|
||||
|
||||
#### Changes
|
||||
|
||||
* **documentation**
|
||||
* #91: Build separate docs for each release
|
||||
* #96: Add complete documentation information to README
|
||||
|
||||
#### Fixed
|
||||
|
||||
* #85: Use address-list for "To", "From" etc.
|
||||
* #93: Force building tests before coverage computing
|
||||
|
||||
<a name="v0.6.0"></a>
|
||||
### v0.6.0 (2016-05-05)
|
||||
|
||||
#### Changes
|
||||
|
||||
* multipart support
|
||||
* add non-consuming methods for Email builders
|
||||
* `add_header` does not return the builder anymore,
|
||||
for consistency with other methods. Use the `header`
|
||||
method instead
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@lettre.rs. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
[version]: https://www.contributor-covenant.org/version/1/4/
|
||||
43
CONTRIBUTING.md
Normal file
43
CONTRIBUTING.md
Normal file
@@ -0,0 +1,43 @@
|
||||
## Contributing to Lettre
|
||||
|
||||
The following guidelines are inspired from the [hyper project](https://github.com/hyperium/hyper/blob/master/CONTRIBUTING.md).
|
||||
|
||||
### Code formatting
|
||||
|
||||
All code must be formatted using `rustfmt`.
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Each commit message consists of a header, a body and a footer. The header has a special format that includes a type, a scope and a subject:
|
||||
|
||||
```text
|
||||
<type>(<scope>): <subject> <BLANK LINE> <body> <BLANK LINE> <footer>
|
||||
```
|
||||
|
||||
Any line of the commit message cannot be longer 72 characters.
|
||||
|
||||
**type** must be one of the following:
|
||||
|
||||
feat: A new feature
|
||||
fix: A bug fix
|
||||
docs: Documentation only changes
|
||||
style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
|
||||
perf: A code change that improves performance
|
||||
|
||||
**scope** is the lettre part that is being touched. Examples:
|
||||
|
||||
email
|
||||
transport-smtp
|
||||
transport-file
|
||||
transport
|
||||
all
|
||||
|
||||
The body explains the change, and the footer contains relevant changelog notes and references to fixed issues.
|
||||
|
||||
### Release process
|
||||
|
||||
Releases are made using `cargo-release`:
|
||||
|
||||
```bash
|
||||
cargo release --dry-run 0.10.0 --prev-tag-name v0.9.2 -v
|
||||
```
|
||||
164
Cargo.toml
164
Cargo.toml
@@ -1,27 +1,155 @@
|
||||
[package]
|
||||
|
||||
name = "smtp"
|
||||
version = "0.3.0"
|
||||
description = "Simple SMTP client"
|
||||
name = "lettre"
|
||||
# remember to update html_root_url and README.md (Cargo.toml example and deps.rs badge)
|
||||
version = "0.10.0-alpha.5"
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
documentation = "http://amousset.me/rust-smtp/smtp/"
|
||||
repository = "https://github.com/amousset/rust-smtp"
|
||||
homepage = "https://lettre.rs"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <alexis.mousset@gmx.fr>"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
authors = ["Alexis Mousset <contact@amousset.me>", "Paolo Barbolini <paolo@paolo565.org>"]
|
||||
categories = ["email", "network-programming"]
|
||||
keywords = ["email", "smtp", "mailer", "message", "sendmail"]
|
||||
edition = "2018"
|
||||
|
||||
[badges]
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
|
||||
[dependencies]
|
||||
time = "0.1"
|
||||
uuid = "0.1"
|
||||
log = "0.3"
|
||||
rustc-serialize = "0.3"
|
||||
rust-crypto = "0.2"
|
||||
bufstream = "0.1"
|
||||
email = "0.0"
|
||||
openssl = "0.6"
|
||||
idna = "0.2"
|
||||
tracing = { version = "0.1.16", default-features = false, features = ["std"], optional = true } # feature
|
||||
|
||||
# builder
|
||||
hyperx = { version = "1", optional = true, features = ["headers"] }
|
||||
mime = { version = "0.3", optional = true }
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
rand = { version = "0.8", optional = true }
|
||||
quoted_printable = { version = "0.4", optional = true }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
once_cell = "1"
|
||||
regex = { version = "1", default-features = false, features = ["std", "unicode-case"] }
|
||||
|
||||
# file transport
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
|
||||
# smtp
|
||||
nom = { version = "6", default-features = false, features = ["alloc"], optional = true }
|
||||
r2d2 = { version = "0.8", optional = true } # feature
|
||||
hostname = { version = "0.3", optional = true } # feature
|
||||
|
||||
## tls
|
||||
native-tls = { version = "0.2", optional = true } # feature
|
||||
rustls = { version = "0.19", features = ["dangerous_configuration"], optional = true }
|
||||
webpki = { version = "0.21", optional = true }
|
||||
webpki-roots = { version = "0.21", 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, features = ["unstable"] }
|
||||
#async-native-tls = { version = "0.3.3", optional = true }
|
||||
async-rustls = { version = "0.2", optional = true }
|
||||
|
||||
## tokio
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["fs", "process", "tcp", "dns", "io-util"], optional = true }
|
||||
tokio02_native_tls_crate = { package = "tokio-native-tls", version = "0.1", optional = true }
|
||||
tokio02_rustls = { package = "tokio-rustls", version = "0.15", optional = true }
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["fs", "process", "net", "io-util"], optional = true }
|
||||
tokio1_native_tls_crate = { package = "tokio-native-tls", version = "0.3", optional = true }
|
||||
tokio1_rustls = { package = "tokio-rustls", version = "0.22", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.3"
|
||||
criterion = "0.3"
|
||||
tracing-subscriber = "0.2.10"
|
||||
glob = "0.3"
|
||||
walkdir = "2"
|
||||
tokio02_crate = { package = "tokio", version = "0.2.7", features = ["macros", "rt-threaded"] }
|
||||
tokio1_crate = { package = "tokio", version = "1", features = ["macros", "rt-multi-thread"] }
|
||||
async-std = { version = "1.8", features = ["attributes"] }
|
||||
serde_json = "1"
|
||||
maud = "0.22.1"
|
||||
|
||||
[[bench]]
|
||||
harness = false
|
||||
name = "transport_smtp"
|
||||
|
||||
[features]
|
||||
unstable = []
|
||||
default = ["file-transport", "file-transport-envelope", "smtp-transport", "native-tls", "hostname", "r2d2", "sendmail-transport", "builder"]
|
||||
builder = ["mime", "base64", "hyperx", "rand", "quoted_printable"]
|
||||
|
||||
# transports
|
||||
file-transport = []
|
||||
file-transport-envelope = ["serde", "serde_json", "file-transport"]
|
||||
sendmail-transport = []
|
||||
smtp-transport = ["base64", "nom"]
|
||||
|
||||
rustls-tls = ["webpki", "webpki-roots", "rustls"]
|
||||
|
||||
# async
|
||||
async-std1 = ["async-std", "async-trait", "futures-io", "futures-util"]
|
||||
#async-std1-native-tls = ["async-std1", "native-tls", "async-native-tls"]
|
||||
async-std1-rustls-tls = ["async-std1", "rustls-tls", "async-rustls"]
|
||||
tokio02 = ["tokio02_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio02-native-tls = ["tokio02", "native-tls", "tokio02_native_tls_crate"]
|
||||
tokio02-rustls-tls = ["tokio02", "rustls-tls", "tokio02_rustls"]
|
||||
tokio1 = ["tokio1_crate", "async-trait", "futures-io", "futures-util"]
|
||||
tokio1-native-tls = ["tokio1", "native-tls", "tokio1_native_tls_crate"]
|
||||
tokio1-rustls-tls = ["tokio1", "rustls-tls", "tokio1_rustls"]
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[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 = "tokio02_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio02_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio02", "tokio02-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_tls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "tokio1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "tokio1", "tokio1-native-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_tls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
[[example]]
|
||||
name = "asyncstd1_smtp_starttls"
|
||||
required-features = ["smtp-transport", "async-std1", "async-std1-rustls-tls", "builder"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,6 @@
|
||||
Copyright (c) 2014 Alexis Mousset
|
||||
Copyright (c) 2014-2020 Alexis Mousset <contact@amousset.me>
|
||||
Copyright (c) 2019-2020 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
|
||||
|
||||
115
README.md
115
README.md
@@ -1,22 +1,117 @@
|
||||
rust-smtp [](https://travis-ci.org/amousset/rust-smtp) [](https://coveralls.io/github/amousset/rust-smtp?branch=master) [](https://crates.io/crates/smtp) [](./LICENSE)
|
||||
=========
|
||||
<h1 align="center">lettre</h1>
|
||||
<div align="center">
|
||||
<strong>
|
||||
A mailer library for Rust
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
This library implements a simple SMTP client.
|
||||
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
|
||||
<br />
|
||||
|
||||
Install
|
||||
-------
|
||||
<div align="center">
|
||||
<a href="https://docs.rs/lettre">
|
||||
<img src="https://docs.rs/lettre/badge.svg"
|
||||
alt="docs" />
|
||||
</a>
|
||||
<a href="https://crates.io/crates/lettre">
|
||||
<img src="https://img.shields.io/crates/d/lettre.svg"
|
||||
alt="downloads" />
|
||||
</a>
|
||||
<br />
|
||||
<a href="https://gitter.im/lettre/lettre">
|
||||
<img src="https://badges.gitter.im/lettre/lettre.svg"
|
||||
alt="chat on gitter" />
|
||||
</a>
|
||||
<a href="https://lettre.rs">
|
||||
<img src="https://img.shields.io/badge/visit-website-blueviolet"
|
||||
alt="website" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://deps.rs/crate/lettre/0.10.0-alpha.4">
|
||||
<img src="https://deps.rs/crate/lettre/0.10.0-alpha.4/status.svg"
|
||||
alt="dependency status" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
**NOTE**: this readme refers to the 0.10 version of lettre, which is
|
||||
still being worked on. The master branch and the alpha releases will see
|
||||
API breaking changes and some features may be missing or incomplete until
|
||||
the stable 0.10.0 release is out.
|
||||
Use the [`v0.9.x`](https://github.com/lettre/lettre/tree/v0.9.x) branch for stable releases.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
Lettre provides the following features:
|
||||
|
||||
* Multiple transport methods
|
||||
* Unicode support (for email content and addresses)
|
||||
* Secure delivery with SMTP using encryption and authentication
|
||||
* Easy email builders
|
||||
* Async support
|
||||
|
||||
Lettre does not provide (for now):
|
||||
|
||||
* Email parsing
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.45 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smtp = "0.2"
|
||||
lettre = "0.10.0-alpha.4"
|
||||
```
|
||||
|
||||
License
|
||||
-------
|
||||
```rust,no_run
|
||||
use lettre::transport::smtp::authentication::Credentials;
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
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")
|
||||
.body("Be happy!")
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// 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),
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
|
||||
|
||||
Alternatively only unit tests can be run by doing `cargo test --lib`.
|
||||
|
||||
## Code of conduct
|
||||
|
||||
Anyone who interacts with Lettre in any space, including but not limited to
|
||||
this GitHub repository, must follow our [code of conduct](https://github.com/lettre/lettre/blob/master/CODE_OF_CONDUCT.md).
|
||||
|
||||
## License
|
||||
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
See LICENSE for details.
|
||||
The builder comes from [emailmessage-rs](https://github.com/katyo/emailmessage-rs) by
|
||||
Kayo, under MIT license.
|
||||
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
9
SECURITY.md
Normal file
9
SECURITY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Report a security issue
|
||||
|
||||
The lettre project team welcomes security reports and is committed to providing prompt attention to security issues.
|
||||
Security issues should be reported privately via [security@lettre.rs](mailto:security@lettre.rs). Security issues
|
||||
should not be reported via the public Github Issue tracker.
|
||||
|
||||
## Security advisories
|
||||
|
||||
Security issues will be announced via the [RustSec advisory database](https://github.com/RustSec/advisory-db).
|
||||
44
benches/transport_smtp.rs
Normal file
44
benches/transport_smtp.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use lettre::{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")
|
||||
.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")
|
||||
.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);
|
||||
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 `tokio02_`, `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
|
||||
31
examples/asyncstd1_smtp_starttls.rs
Normal file
31
examples/asyncstd1_smtp_starttls.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Connector,
|
||||
AsyncStd1Transport, 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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = AsyncSmtpTransport::<AsyncStd1Connector>::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),
|
||||
}
|
||||
}
|
||||
31
examples/asyncstd1_smtp_tls.rs
Normal file
31
examples/asyncstd1_smtp_tls.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, AsyncStd1Connector,
|
||||
AsyncStd1Transport, 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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = AsyncSmtpTransport::<AsyncStd1Connector>::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),
|
||||
}
|
||||
}
|
||||
51
examples/basic_html.rs
Normal file
51
examples/basic_html.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use lettre::message::{header, MultiPart, SinglePart};
|
||||
use lettre::{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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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");
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate smtp;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use smtp::sender::SenderBuilder;
|
||||
use smtp::email::EmailBuilder;
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
|
||||
let sender = Arc::new(Mutex::new(SenderBuilder::localhost().unwrap().hello_name("localhost")
|
||||
.enable_connection_reuse(true).build()));
|
||||
|
||||
let mut threads = Vec::new();
|
||||
for _ in 1..5 {
|
||||
|
||||
let th_sender = sender.clone();
|
||||
threads.push(thread::spawn(move || {
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello")
|
||||
.build();
|
||||
|
||||
let _ = th_sender.lock().unwrap().send(email);
|
||||
}));
|
||||
}
|
||||
|
||||
for thread in threads {
|
||||
let _ = thread.join();
|
||||
}
|
||||
|
||||
let email = EmailBuilder::new()
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.body("Hello World!")
|
||||
.subject("Hello Bis")
|
||||
.build();
|
||||
|
||||
let mut sender = sender.lock().unwrap();
|
||||
let result = sender.send(email);
|
||||
sender.close();
|
||||
|
||||
match result {
|
||||
Ok(..) => info!("Email sent successfully"),
|
||||
Err(error) => error!("{:?}", error),
|
||||
}
|
||||
}
|
||||
60
examples/maud_html.rs
Normal file
60
examples/maud_html.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
use lettre::message::{header, MultiPart, SinglePart};
|
||||
use lettre::{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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.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");
|
||||
}
|
||||
22
examples/smtp.rs
Normal file
22
examples/smtp.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use lettre::{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")
|
||||
.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),
|
||||
}
|
||||
}
|
||||
41
examples/smtp_selfsigned.rs
Normal file
41
examples/smtp_selfsigned.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use std::fs;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials,
|
||||
transport::smtp::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")
|
||||
.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 mut tls = TlsParameters::builder("smtp.server.com".to_string());
|
||||
tls.add_root_certificate(cert);
|
||||
let tls = tls.build().unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
27
examples/smtp_starttls.rs
Normal file
27
examples/smtp_starttls.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use lettre::{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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
27
examples/smtp_tls.rs
Normal file
27
examples/smtp_tls.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use lettre::{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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
36
examples/tokio02_smtp_starttls.rs
Normal file
36
examples/tokio02_smtp_starttls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = AsyncSmtpTransport::<Tokio02Connector>::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),
|
||||
}
|
||||
}
|
||||
36
examples/tokio02_smtp_tls.rs
Normal file
36
examples/tokio02_smtp_tls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 tokio02_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio02Connector,
|
||||
Tokio02Transport,
|
||||
};
|
||||
|
||||
#[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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = AsyncSmtpTransport::<Tokio02Connector>::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),
|
||||
}
|
||||
}
|
||||
36
examples/tokio1_smtp_starttls.rs
Normal file
36
examples/tokio1_smtp_starttls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Connector,
|
||||
Tokio1Transport,
|
||||
};
|
||||
|
||||
#[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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail using STARTTLS
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Connector>::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),
|
||||
}
|
||||
}
|
||||
36
examples/tokio1_smtp_tls.rs
Normal file
36
examples/tokio1_smtp_tls.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 tokio1_crate as tokio;
|
||||
|
||||
use lettre::{
|
||||
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Message, Tokio1Connector,
|
||||
Tokio1Transport,
|
||||
};
|
||||
|
||||
#[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")
|
||||
.body(String::from("Be happy with async!"))
|
||||
.unwrap();
|
||||
|
||||
let creds = Credentials::new("smtp_username".to_string(), "smtp_password".to_string());
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mailer = AsyncSmtpTransport::<Tokio1Connector>::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),
|
||||
}
|
||||
}
|
||||
146
src/address/envelope.rs
Normal file
146
src/address/envelope.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
#[cfg(feature = "builder")]
|
||||
use std::convert::TryFrom;
|
||||
|
||||
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.
|
||||
forward_path: Vec<Address>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<Address>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
///
|
||||
/// 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
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use lettre::address::Envelope;
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let sender = Address::from_str("from@email.com")?;
|
||||
/// let recipients = vec![Address::from_str("to@email.com")?];
|
||||
///
|
||||
/// 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 = "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(header::Sender(a)) => Some(a.email.clone()),
|
||||
// ... else try From
|
||||
None => match headers.get::<header::From>() {
|
||||
Some(header::From(a)) => {
|
||||
let from: Vec<Mailbox> = a.clone().into();
|
||||
if from.len() > 1 {
|
||||
return Err(Error::TooManyFrom);
|
||||
}
|
||||
Some(from[0].email.clone())
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
};
|
||||
|
||||
fn add_addresses_from_mailboxes(
|
||||
addresses: &mut Vec<Address>,
|
||||
mailboxes: Option<&Mailboxes>,
|
||||
) {
|
||||
if let Some(mailboxes) = mailboxes {
|
||||
for mailbox in mailboxes.iter() {
|
||||
addresses.push(mailbox.email.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
8
src/address/mod.rs
Normal file
8
src/address/mod.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
|
||||
mod envelope;
|
||||
mod types;
|
||||
|
||||
pub use self::envelope::Envelope;
|
||||
pub use self::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<'de> Visitor<'de> 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)
|
||||
}
|
||||
}
|
||||
251
src/address/types.rs
Normal file
251
src/address/types.rs
Normal file
@@ -0,0 +1,251 @@
|
||||
//! Representation of an email address
|
||||
|
||||
use idna::domain_to_ascii;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
error::Error,
|
||||
ffi::OsStr,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
net::IpAddr,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// 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 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("example", "email.com")?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// You can also create an `Address` from a string literal by parsing it:
|
||||
///
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
/// # use lettre::Address;
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let address = Address::from_str("example@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<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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Regex from the specs
|
||||
// https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address
|
||||
// It will mark esoteric email addresses like quoted string as invalid
|
||||
static USER_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+\z").unwrap());
|
||||
static DOMAIN_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(
|
||||
r"(?i)^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
// literal form, ipv4 or ipv6 address (SMTP 4.1.3)
|
||||
static LITERAL_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?i)\[([A-f0-9:\.]+)\]\z").unwrap());
|
||||
|
||||
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("example", "email.com")?;
|
||||
/// let expected: Address = "example@email.com".parse()?;
|
||||
/// 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("example", "email.com")?;
|
||||
/// assert_eq!("example", address.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("example", "email.com")?;
|
||||
/// assert_eq!("email.com", address.domain());
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.serialized[self.at_start + 1..]
|
||||
}
|
||||
|
||||
pub(super) fn check_user(user: &str) -> Result<(), AddressError> {
|
||||
if USER_RE.is_match(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> {
|
||||
if DOMAIN_RE.is_match(domain) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(caps) = LITERAL_RE.captures(domain) {
|
||||
if let Some(cap) = caps.get(1) {
|
||||
if cap.as_str().parse::<IpAddr>().is_ok() {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(AddressError::InvalidDomain)
|
||||
}
|
||||
}
|
||||
|
||||
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 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(Address {
|
||||
serialized: val.into(),
|
||||
at_start: user.len(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum AddressError {
|
||||
MissingParts,
|
||||
Unbalanced,
|
||||
InvalidUser,
|
||||
InvalidDomain,
|
||||
InvalidUtf8b,
|
||||
}
|
||||
|
||||
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::InvalidUtf8b => f.write_str("Invalid UTF8b data"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_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");
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
//! Provides authentication mecanisms
|
||||
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
|
||||
use serialize::base64::{self, ToBase64, FromBase64};
|
||||
use serialize::hex::ToHex;
|
||||
use crypto::hmac::Hmac;
|
||||
use crypto::md5::Md5;
|
||||
use crypto::mac::Mac;
|
||||
|
||||
use NUL;
|
||||
use error::Error;
|
||||
|
||||
/// Represents authentication mecanisms
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Hash,Debug)]
|
||||
pub enum Mecanism {
|
||||
/// PLAIN authentication mecanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// CRAM-MD5 authentication mecanism
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5,
|
||||
}
|
||||
|
||||
impl Display for Mecanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mecanism::Plain => "PLAIN",
|
||||
Mecanism::CramMd5 => "CRAM-MD5",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mecanism {
|
||||
/// Does the mecanism supports initial response
|
||||
pub fn supports_initial_response(&self) -> bool {
|
||||
match *self {
|
||||
Mecanism::Plain => true,
|
||||
Mecanism::CramMd5 => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string to send to the server, using the provided username, password and
|
||||
/// challenge in some cases
|
||||
pub fn response(&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
challenge: Option<&str>)
|
||||
-> Result<String, Error> {
|
||||
match *self {
|
||||
Mecanism::Plain => {
|
||||
match challenge {
|
||||
Some(_) => Err(Error::ClientError("This mecanism does not expect a challenge")),
|
||||
None => Ok(format!("{}{}{}{}", NUL, username, NUL, password)
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD)),
|
||||
}
|
||||
}
|
||||
Mecanism::CramMd5 => {
|
||||
let encoded_challenge = match challenge {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ClientError("This mecanism does expect a challenge")),
|
||||
};
|
||||
|
||||
let decoded_challenge = match encoded_challenge.from_base64() {
|
||||
Ok(challenge) => challenge,
|
||||
Err(error) => return Err(Error::ChallengeParsingError(error)),
|
||||
};
|
||||
|
||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||
hmac.input(&decoded_challenge);
|
||||
|
||||
Ok(format!("{} {}", username, hmac.result().code().to_hex())
|
||||
.as_bytes()
|
||||
.to_base64(base64::STANDARD))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mecanism;
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mecanism = Mecanism::Plain;
|
||||
|
||||
assert_eq!(mecanism.response("username", "password", None).unwrap(),
|
||||
"AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
assert!(mecanism.response("username", "password", Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
let mecanism = Mecanism::CramMd5;
|
||||
|
||||
assert_eq!(mecanism.response("alice",
|
||||
"wonderland",
|
||||
Some("PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg=="))
|
||||
.unwrap(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
assert!(mecanism.response("alice", "wonderland", Some("tést")).is_err());
|
||||
assert!(mecanism.response("alice", "wonderland", None).is_err());
|
||||
}
|
||||
}
|
||||
@@ -1,274 +0,0 @@
|
||||
//! SMTP client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::io::{BufRead, Read, Write};
|
||||
use std::io;
|
||||
use std::fmt::Debug;
|
||||
|
||||
use bufstream::BufStream;
|
||||
use openssl::ssl::SslContext;
|
||||
|
||||
use response::ResponseParser;
|
||||
use authentication::Mecanism;
|
||||
use error::{Error, SmtpResult};
|
||||
use client::net::{Connector, NetworkStream};
|
||||
use {CRLF, MESSAGE_ENDING};
|
||||
|
||||
pub mod net;
|
||||
|
||||
/// Returns the string after adding a dot at the beginning of each line starting with a dot
|
||||
///
|
||||
/// Reference : https://tools.ietf.org/html/rfc5321#page-62 (4.5.2. Transparency)
|
||||
#[inline]
|
||||
fn escape_dot(string: &str) -> String {
|
||||
if string.starts_with(".") {
|
||||
format!(".{}", string)
|
||||
} else {
|
||||
string.to_string()
|
||||
}
|
||||
.replace("\r.", "\r..")
|
||||
.replace("\n.", "\n..")
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
#[inline]
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace(CRLF, "<CR><LF>")
|
||||
}
|
||||
|
||||
/// Returns the string removing all the CRLF
|
||||
#[inline]
|
||||
fn remove_crlf(string: &str) -> String {
|
||||
string.replace(CRLF, "")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct Client<S: Write + Read = NetworkStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
}
|
||||
|
||||
macro_rules! return_err (
|
||||
($err: expr, $client: ident) => ({
|
||||
return Err(From::from($err))
|
||||
})
|
||||
);
|
||||
|
||||
impl<S: Write + Read = NetworkStream> Client<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> Client<S> {
|
||||
Client { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Debug + Clone = NetworkStream> Client<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.quit();
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: S) {
|
||||
self.stream = Some(BufStream::new(stream));
|
||||
}
|
||||
|
||||
/// Upgrades the underlying connection to SSL/TLS
|
||||
pub fn upgrade_tls_stream(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
//let current_stream = self.stream.clone();
|
||||
if self.stream.is_some() {
|
||||
self.stream.as_mut().unwrap().get_mut().upgrade_tls(ssl_context)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(&mut self, addr: &A) -> 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 = try!(addr.to_socket_addrs());
|
||||
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(try!(Connector::connect(&server_addr, None)));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.noop().is_ok()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command(&mut self, command: &str) -> SmtpResult {
|
||||
self.send_server(command, CRLF)
|
||||
}
|
||||
|
||||
/// Sends a EHLO command
|
||||
pub fn ehlo(&mut self, hostname: &str) -> SmtpResult {
|
||||
self.command(&format!("EHLO {}", hostname))
|
||||
}
|
||||
|
||||
/// Sends a MAIL command
|
||||
pub fn mail(&mut self, address: &str, options: Option<&str>) -> SmtpResult {
|
||||
match options {
|
||||
Some(ref options) => self.command(&format!("MAIL FROM:<{}> {}", address, options)),
|
||||
None => self.command(&format!("MAIL FROM:<{}>", address)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a RCPT command
|
||||
pub fn rcpt(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("RCPT TO:<{}>", address))
|
||||
}
|
||||
|
||||
/// Sends a DATA command
|
||||
pub fn data(&mut self) -> SmtpResult {
|
||||
self.command("DATA")
|
||||
}
|
||||
|
||||
/// Sends a QUIT command
|
||||
pub fn quit(&mut self) -> SmtpResult {
|
||||
self.command("QUIT")
|
||||
}
|
||||
|
||||
/// Sends a NOOP command
|
||||
pub fn noop(&mut self) -> SmtpResult {
|
||||
self.command("NOOP")
|
||||
}
|
||||
|
||||
/// Sends a HELP command
|
||||
pub fn help(&mut self, argument: Option<&str>) -> SmtpResult {
|
||||
match argument {
|
||||
Some(ref argument) => self.command(&format!("HELP {}", argument)),
|
||||
None => self.command("HELP"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a VRFY command
|
||||
pub fn vrfy(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("VRFY {}", address))
|
||||
}
|
||||
|
||||
/// Sends a EXPN command
|
||||
pub fn expn(&mut self, address: &str) -> SmtpResult {
|
||||
self.command(&format!("EXPN {}", address))
|
||||
}
|
||||
|
||||
/// Sends a RSET command
|
||||
pub fn rset(&mut self) -> SmtpResult {
|
||||
self.command("RSET")
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mecanism
|
||||
pub fn auth(&mut self, mecanism: Mecanism, username: &str, password: &str) -> SmtpResult {
|
||||
|
||||
if mecanism.supports_initial_response() {
|
||||
self.command(&format!("AUTH {} {}",
|
||||
mecanism,
|
||||
try!(mecanism.response(username, password, None))))
|
||||
} else {
|
||||
let encoded_challenge = match try!(self.command("AUTH CRAM-MD5")).first_word() {
|
||||
Some(challenge) => challenge,
|
||||
None => return Err(Error::ResponseParsingError("Could not read CRAM challenge")),
|
||||
};
|
||||
|
||||
debug!("CRAM challenge: {}", encoded_challenge);
|
||||
|
||||
let cram_response = try!(mecanism.response(username,
|
||||
password,
|
||||
Some(&encoded_challenge)));
|
||||
|
||||
self.command(&format!("{}", cram_response))
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a STARTTLS command
|
||||
pub fn starttls(&mut self) -> SmtpResult {
|
||||
self.command("STARTTLS")
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message_content: &str) -> SmtpResult {
|
||||
self.send_server(&escape_dot(message_content), MESSAGE_ENDING)
|
||||
}
|
||||
|
||||
/// Sends a string to the server and gets the response
|
||||
fn send_server(&mut self, string: &str, end: &str) -> SmtpResult {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
try!(write!(self.stream.as_mut().unwrap(), "{}{}", string, end));
|
||||
try!(self.stream.as_mut().unwrap().flush());
|
||||
|
||||
debug!("Wrote: {}", escape_crlf(string));
|
||||
|
||||
self.get_reply()
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn get_reply(&mut self) -> SmtpResult {
|
||||
|
||||
let mut parser = ResponseParser::new();
|
||||
|
||||
let mut line = String::new();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
|
||||
debug!("Read: {}", escape_crlf(line.as_ref()));
|
||||
|
||||
while try!(parser.read_line(remove_crlf(line.as_ref()).as_ref())) {
|
||||
line.clear();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
}
|
||||
|
||||
let response = try!(parser.response());
|
||||
|
||||
match response.is_positive() {
|
||||
true => Ok(response),
|
||||
false => Err(From::from(response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_dot, remove_crlf, escape_crlf};
|
||||
|
||||
#[test]
|
||||
fn test_escape_dot() {
|
||||
assert_eq!(escape_dot(".test"), "..test");
|
||||
assert_eq!(escape_dot("\r.\n.\r\n"), "\r..\n..\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.test\r\n"), "test\r\n..test\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.\r\ntest"), "test\r\n..\r\ntest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_crlf() {
|
||||
assert_eq!(remove_crlf("\r\n"), "");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\n"), "EHLO my_name");
|
||||
assert_eq!(remove_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_nameSIZE 42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>");
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use std::io;
|
||||
use std::io::{Read, Write, ErrorKind};
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpStream;
|
||||
use std::fmt;
|
||||
use std::fmt::{Debug, Formatter};
|
||||
use openssl::ssl::{SslContext, SslStream};
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connector {
|
||||
/// Opens a connection to the given IP socket
|
||||
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<Self>;
|
||||
/// Upgrades to TLS connection
|
||||
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(addr: &SocketAddr, ssl_context: Option<&SslContext>) -> io::Result<NetworkStream> {
|
||||
let tcp_stream = try!(TcpStream::connect(addr));
|
||||
|
||||
match ssl_context {
|
||||
Some(context) => match SslStream::new(&context, tcp_stream) {
|
||||
Ok(stream) => Ok(NetworkStream::Ssl(stream)),
|
||||
Err(err) => Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
None => Ok(NetworkStream::Plain(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
fn upgrade_tls(&mut self, ssl_context: &SslContext) -> io::Result<()> {
|
||||
*self = match self.clone() {
|
||||
NetworkStream::Plain(stream) => match SslStream::new(ssl_context, stream) {
|
||||
Ok(ssl_stream) => NetworkStream::Ssl(ssl_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Ssl(stream) => NetworkStream::Ssl(stream),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
pub enum NetworkStream {
|
||||
/// Plain TCP
|
||||
Plain(TcpStream),
|
||||
/// SSL over TCP
|
||||
Ssl(SslStream<TcpStream>),
|
||||
}
|
||||
|
||||
impl Clone for NetworkStream {
|
||||
#[inline]
|
||||
fn clone(&self) -> NetworkStream {
|
||||
match self {
|
||||
&NetworkStream::Plain(ref stream) => NetworkStream::Plain(stream.try_clone().unwrap()),
|
||||
&NetworkStream::Ssl(ref stream) => NetworkStream::Ssl(stream.try_clone().unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for NetworkStream {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NetworkStream(_)")
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
#[inline]
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.read(buf),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
#[inline]
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.write(msg),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.write(msg),
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Plain(ref mut stream) => stream.flush(),
|
||||
NetworkStream::Ssl(ref mut stream) => stream.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
348
src/email.rs
348
src/email.rs
@@ -1,348 +0,0 @@
|
||||
//! Simple email (very incomplete)
|
||||
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
use email_format::{MimeMessage, Header, Mailbox};
|
||||
use time::{now, Tm};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Address`
|
||||
pub trait ToHeader {
|
||||
/// Converts to a `Header` struct
|
||||
fn to_header(&self) -> Header;
|
||||
}
|
||||
|
||||
impl ToHeader for Header {
|
||||
fn to_header(&self) -> Header {
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToHeader for (&'a str, &'a str) {
|
||||
fn to_header(&self) -> Header {
|
||||
let (name, value) = *self;
|
||||
Header::new(name.to_string(), value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts an adress or an address with an alias to a `Mailbox`
|
||||
pub trait ToMailbox {
|
||||
/// Converts to a `Mailbox` struct
|
||||
fn to_mailbox(&self) -> Mailbox;
|
||||
}
|
||||
|
||||
impl ToMailbox for Mailbox {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToMailbox for &'a str {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
Mailbox::new(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToMailbox for (&'a str, &'a str) {
|
||||
fn to_mailbox(&self) -> Mailbox {
|
||||
let (address, alias) = *self;
|
||||
Mailbox::new_with_name(alias.to_string(), address.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct EmailBuilder {
|
||||
/// Email content
|
||||
content: Email,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
/// The enveloppe recipients addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
from: Option<String>,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
|
||||
impl Display for Email {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f, "{}", self.message.as_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
let current_message = Uuid::new_v4();
|
||||
|
||||
let mut email = Email {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
to: vec![],
|
||||
from: None,
|
||||
message_id: current_message,
|
||||
};
|
||||
|
||||
match Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}@rust-smtp>", current_message)) {
|
||||
Ok(header) => email.message.headers.insert(header),
|
||||
Err(_) => (),
|
||||
}
|
||||
|
||||
EmailBuilder {
|
||||
content: email,
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body(mut self, body: &str) -> EmailBuilder {
|
||||
self.content.message.body = body.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||
self.insert_header(header);
|
||||
self
|
||||
}
|
||||
|
||||
fn insert_header<A: ToHeader>(&mut self, header: A) {
|
||||
self.content.message.headers.insert(header.to_header());
|
||||
}
|
||||
|
||||
/// Adds a `From` header and store the sender address
|
||||
pub fn from<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("From", mailbox.to_string().as_ref()));
|
||||
self.content.from = Some(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and store the recipient address
|
||||
pub fn to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("To", mailbox.to_string().as_ref()));
|
||||
self.content.to.push(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and store the recipient address
|
||||
pub fn cc<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Cc", mailbox.to_string().as_ref()));
|
||||
self.content.to.push(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Reply-To", mailbox.to_string().as_ref()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: ToMailbox>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.to_mailbox();
|
||||
self.insert_header(("Sender", mailbox.to_string().as_ref()));
|
||||
self.content.from = Some(mailbox.address);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
||||
self.insert_header(("Subject", subject));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.insert_header(("Date", Tm::rfc822z(date).to_string().as_ref()));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Email
|
||||
pub fn build(mut self) -> Email {
|
||||
if !self.date_issued {
|
||||
self.insert_header(("Date", Tm::rfc822z(&now()).to_string().as_ref()));
|
||||
}
|
||||
self.content.message.update_headers();
|
||||
self.content
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Email sendable by an SMTP client
|
||||
pub trait SendableEmail {
|
||||
/// From address
|
||||
fn from_address(&self) -> Option<String>;
|
||||
/// To addresses
|
||||
fn to_addresses(&self) -> Option<Vec<String>>;
|
||||
/// Message content
|
||||
fn message(&self) -> Option<String>;
|
||||
/// Message ID
|
||||
fn message_id(&self) -> Option<String>;
|
||||
}
|
||||
|
||||
/// Minimal email structure
|
||||
pub struct SimpleSendableEmail {
|
||||
/// From address
|
||||
from: String,
|
||||
/// To addresses
|
||||
to: Vec<String>,
|
||||
/// Message
|
||||
message: String,
|
||||
}
|
||||
|
||||
impl SimpleSendableEmail {
|
||||
/// Returns a new email
|
||||
pub fn new(from_address: &str, to_address: &str, message: &str) -> SimpleSendableEmail {
|
||||
SimpleSendableEmail {
|
||||
from: from_address.to_string(),
|
||||
to: vec![to_address.to_string()],
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SendableEmail for SimpleSendableEmail {
|
||||
fn from_address(&self) -> Option<String> {
|
||||
Some(self.from.clone())
|
||||
}
|
||||
|
||||
fn to_addresses(&self) -> Option<Vec<String>> {
|
||||
Some(self.to.clone())
|
||||
}
|
||||
|
||||
fn message(&self) -> Option<String> {
|
||||
Some(self.message.clone())
|
||||
}
|
||||
|
||||
fn message_id(&self) -> Option<String> {
|
||||
Some(format!("<{}@rust-smtp>", Uuid::new_v4()))
|
||||
}
|
||||
}
|
||||
|
||||
impl SendableEmail for Email {
|
||||
/// Return the to addresses, and fails if it is not set
|
||||
fn to_addresses(&self) -> Option<Vec<String>> {
|
||||
if self.to.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.to.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the from address, and fails if it is not set
|
||||
fn from_address(&self) -> Option<String> {
|
||||
match self.from {
|
||||
Some(ref from_address) => Some(from_address.clone()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn message(&self) -> Option<String> {
|
||||
Some(format!("{}", self))
|
||||
}
|
||||
|
||||
fn message_id(&self) -> Option<String> {
|
||||
Some(format!("{}", self.message_id))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use time::now;
|
||||
|
||||
use uuid::Uuid;
|
||||
use email_format::{MimeMessage, Header};
|
||||
|
||||
use super::{SendableEmail, EmailBuilder, Email};
|
||||
|
||||
#[test]
|
||||
fn test_email_display() {
|
||||
let current_message = Uuid::new_v4();
|
||||
|
||||
let mut email = Email {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
to: vec![],
|
||||
from: None,
|
||||
message_id: current_message,
|
||||
};
|
||||
|
||||
email.message.headers.insert(Header::new_with_value("Message-ID".to_string(),
|
||||
format!("<{}@rust-smtp>",
|
||||
current_message))
|
||||
.unwrap());
|
||||
|
||||
email.message
|
||||
.headers
|
||||
.insert(Header::new_with_value("To".to_string(), "to@example.com".to_string())
|
||||
.unwrap());
|
||||
|
||||
email.message.body = "body".to_string();
|
||||
|
||||
assert_eq!(format!("{}", email),
|
||||
format!("Message-ID: <{}@rust-smtp>\r\nTo: to@example.com\r\n\r\nbody\r\n",
|
||||
current_message));
|
||||
assert_eq!(current_message.to_string(), email.message_id().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.build();
|
||||
|
||||
assert_eq!(format!("{}", email),
|
||||
format!("Message-ID: <{}@rust-smtp>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\nReply-To: \
|
||||
<reply@localhost>\r\nSender: <sender@localhost>\r\nDate: \
|
||||
{}\r\nSubject: Hello\r\nX-test: value\r\n\r\nHello World!\r\n",
|
||||
email.message_id().unwrap(),
|
||||
date_now.rfc822z()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email = email_builder.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.add_header(("X-test", "value"))
|
||||
.build();
|
||||
|
||||
assert_eq!(email.from_address().unwrap(),
|
||||
"sender@localhost".to_string());
|
||||
assert_eq!(email.to_addresses().unwrap(),
|
||||
vec!["user@localhost".to_string(), "cc@localhost".to_string()]);
|
||||
assert_eq!(email.message().unwrap(), format!("{}", email));
|
||||
}
|
||||
|
||||
}
|
||||
114
src/error.rs
114
src/error.rs
@@ -1,90 +1,52 @@
|
||||
//! Error and result type for SMTP clients
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
|
||||
use std::error::Error as StdError;
|
||||
use std::io;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
|
||||
use response::{Severity, Response};
|
||||
use serialize::base64::FromBase64Error;
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
// FIXME message-specific errors
|
||||
/// Error type for email content
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
TransientError(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
PermanentError(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsingError(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsingError(FromBase64Error),
|
||||
/// Internal client error
|
||||
ClientError(&'static str),
|
||||
/// DNS resolution error
|
||||
ResolutionError,
|
||||
/// 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
|
||||
IoError(io::Error),
|
||||
Io(std::io::Error),
|
||||
/// Non-ASCII chars
|
||||
NonAsciiChars,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
TransientError(_) => "a transient error occured during the SMTP transaction",
|
||||
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
||||
ResponseParsingError(_) => "an error occured while parsing an SMTP response",
|
||||
ChallengeParsingError(_) => "an error occured while parsing a CRAM-MD5 challenge",
|
||||
ResolutionError => "Could no resolve hostname",
|
||||
ClientError(_) => "an unknown error occured",
|
||||
IoError(_) => "an I/O error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
IoError(ref err) => Some(&*err as &StdError),
|
||||
_ => None,
|
||||
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<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
IoError(err)
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(err: std::io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.severity() {
|
||||
Severity::TransientNegativeCompletion => TransientError(response),
|
||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||
_ => ClientError("Unknown error code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
ClientError(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// TODO
|
||||
}
|
||||
impl StdError for Error {}
|
||||
|
||||
220
src/extension.rs
220
src/extension.rs
@@ -1,220 +0,0 @@
|
||||
//! ESMTP features
|
||||
|
||||
use std::result::Result;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use response::Response;
|
||||
use error::Error;
|
||||
use authentication::Mecanism;
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq,Eq,Hash,Clone,Debug)]
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
/// RFC 6152: https://tools.ietf.org/html/rfc6152
|
||||
EightBitMime,
|
||||
/// SMTPUTF8 keyword
|
||||
///
|
||||
/// RFC 6531: https://tools.ietf.org/html/rfc6531
|
||||
SmtpUtfEight,
|
||||
/// STARTTLS keyword
|
||||
///
|
||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||
StartTls,
|
||||
/// AUTH mecanism
|
||||
Authentication(Mecanism),
|
||||
}
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Extension::EightBitMime => write!(f, "{}", "8BITMIME"),
|
||||
Extension::SmtpUtfEight => write!(f, "{}", "SMTPUTF8"),
|
||||
Extension::StartTls => write!(f, "{}", "STARTTLS"),
|
||||
Extension::Authentication(ref mecanism) => write!(f, "{} {}", "AUTH", mecanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone,Debug,Eq,PartialEq)]
|
||||
pub struct ServerInfo {
|
||||
/// Server name
|
||||
///
|
||||
/// The name given in the server banner
|
||||
pub name: String,
|
||||
/// ESMTP features supported by the server
|
||||
///
|
||||
/// It contains the features supported by the server and known by the `Extension` module.
|
||||
pub features: HashSet<Extension>,
|
||||
}
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
match self.features.is_empty() {
|
||||
true => "no supported features".to_string(),
|
||||
false => format!("{:?}", self.features),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerInfo {
|
||||
/// Parses a response to create a `ServerInfo`
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(Error::ResponseParsingError("Could not read server name")),
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
|
||||
for line in response.message() {
|
||||
|
||||
let splitted: Vec<&str> = line.split_whitespace().collect();
|
||||
let _ = match splitted[0] {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
"SMTPUTF8" => {
|
||||
features.insert(Extension::SmtpUtfEight);
|
||||
}
|
||||
"STARTTLS" => {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for &mecanism in &splitted[1..] {
|
||||
match mecanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mecanism::Plain));
|
||||
}
|
||||
"CRAM-MD5" => {
|
||||
features.insert(Extension::Authentication(Mecanism::CramMd5));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name,
|
||||
features: features,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_feature(&self, keyword: &Extension) -> bool {
|
||||
self.features.contains(keyword)
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mecanism(&self, mecanism: Mecanism) -> bool {
|
||||
self.features.contains(&Extension::Authentication(mecanism))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{ServerInfo, Extension};
|
||||
use authentication::Mecanism;
|
||||
use response::{Code, Response, Severity, Category};
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string());
|
||||
assert_eq!(format!("{}", Extension::Authentication(Mecanism::Plain)),
|
||||
"AUTH PLAIN".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo_fmt() {
|
||||
let mut eightbitmime = HashSet::new();
|
||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime.clone(),
|
||||
}),
|
||||
"name with {EightBitMime}".to_string());
|
||||
|
||||
let empty = HashSet::new();
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}),
|
||||
"name with no supported features".to_string());
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mecanism::Plain)));
|
||||
|
||||
assert_eq!(format!("{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain.clone(),
|
||||
}),
|
||||
"name with {Authentication(Plain)}".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo() {
|
||||
let response = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
1),
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features: features,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||
|
||||
assert!(server_info.supports_feature(&Extension::EightBitMime));
|
||||
assert!(!server_info.supports_feature(&Extension::StartTls));
|
||||
assert!(!server_info.supports_auth_mecanism(Mecanism::CramMd5));
|
||||
|
||||
let response2 = Response::new(Code::new(Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
1),
|
||||
vec!["me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]);
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mecanism::Plain)));
|
||||
assert!(features2.insert(Extension::Authentication(Mecanism::CramMd5)));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features: features2,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||
|
||||
assert!(server_info2.supports_feature(&Extension::EightBitMime));
|
||||
assert!(server_info2.supports_auth_mecanism(Mecanism::Plain));
|
||||
assert!(server_info2.supports_auth_mecanism(Mecanism::CramMd5));
|
||||
assert!(!server_info2.supports_feature(&Extension::StartTls));
|
||||
}
|
||||
}
|
||||
381
src/lib.rs
381
src/lib.rs
@@ -1,186 +1,229 @@
|
||||
//! # Rust SMTP client
|
||||
//! Lettre is an email library that allows creating and sending messages. It provides:
|
||||
//!
|
||||
//! This client should tend to follow [RFC 5321](https://tools.ietf.org/html/rfc5321), but is still
|
||||
//! a work in progress. It is designed to efficiently send emails from an application to a
|
||||
//! relay email server, as it relies as much as possible on the relay server for sanity and RFC
|
||||
//! compliance checks.
|
||||
//! * An easy to use email builder
|
||||
//! * Pluggable email transports
|
||||
//! * Unicode support
|
||||
//! * Secure defaults
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//! Lettre requires Rust 1.45 or newer.
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN and CRAM-MD5 mecanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! ## Optional features
|
||||
//!
|
||||
//! It will eventually implement the following extensions:
|
||||
//!
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! This client is divided into three main parts:
|
||||
//!
|
||||
//! * client: a low level SMTP client providing all SMTP commands
|
||||
//! * sender: a high level SMTP client providing an easy method to send emails
|
||||
//! * email: generates the email to be sent with the sender
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ### Simple example
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::sender::{Sender, SenderBuilder};
|
||||
//! use smtp::email::EmailBuilder;
|
||||
//!
|
||||
//! // Create an email
|
||||
//! let email = EmailBuilder::new()
|
||||
//! // Addresses can be specified by the couple (email, alias)
|
||||
//! .to(("user@example.org", "Firstname Lastname"))
|
||||
//! // ... or by an address only
|
||||
//! .from("user@example.com")
|
||||
//! .subject("Hi, Hello world")
|
||||
//! .body("Hello world.")
|
||||
//! .build();
|
||||
//!
|
||||
//! // Open a local connection on port 25
|
||||
//! let mut sender = SenderBuilder::localhost().unwrap().build();
|
||||
//! // Send the email
|
||||
//! let result = sender.send(email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ### Complete example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::sender::{Sender, SenderBuilder};
|
||||
//! use smtp::email::EmailBuilder;
|
||||
//! use smtp::authentication::Mecanism;
|
||||
//! use smtp::SUBMISSION_PORT;
|
||||
//!
|
||||
//! let mut builder = EmailBuilder::new();
|
||||
//! builder = builder.to(("user@example.org", "Alias name"));
|
||||
//! builder = builder.cc(("user@example.net", "Alias name"));
|
||||
//! builder = builder.from("no-reply@example.com");
|
||||
//! builder = builder.from("no-reply@example.eu");
|
||||
//! builder = builder.sender("no-reply@example.com");
|
||||
//! builder = builder.subject("Hello world");
|
||||
//! builder = builder.body("Hi, Hello world.");
|
||||
//! builder = builder.reply_to("contact@example.com");
|
||||
//! builder = builder.add_header(("X-Custom-Header", "my header"));
|
||||
//!
|
||||
//! let email = builder.build();
|
||||
//!
|
||||
//! // Connect to a remote server on a custom port
|
||||
//! let mut sender = SenderBuilder::new(("server.tld", SUBMISSION_PORT)).unwrap()
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name("my.hostname.tld")
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials("username", "password")
|
||||
//! // Use TLS with STARTTLS, you can also specify a specific SSL context
|
||||
//! // with `.ssl_context(context)`
|
||||
//! .starttls()
|
||||
//! // Configure accepted authetication mecanisms
|
||||
//! .authentication_mecanisms(vec![Mecanism::CramMd5])
|
||||
//! // Enable connection reuse
|
||||
//! .enable_connection_reuse(true).build();
|
||||
//!
|
||||
//! let result_1 = sender.send(email.clone());
|
||||
//! assert!(result_1.is_ok());
|
||||
//!
|
||||
//! // The second email will use the same connection
|
||||
//! let result_2 = sender.send(email);
|
||||
//! assert!(result_2.is_ok());
|
||||
//!
|
||||
//! // Explicitely close the SMTP transaction as we enabled connection reuse
|
||||
//! sender.close();
|
||||
//! ```
|
||||
//!
|
||||
//! ### Using the client directly
|
||||
//!
|
||||
//! If you just want to send an email without using `Email` to provide headers:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::sender::{Sender, SenderBuilder};
|
||||
//! use smtp::email::SimpleSendableEmail;
|
||||
//!
|
||||
//! // Create a minimal email
|
||||
//! let email = SimpleSendableEmail::new(
|
||||
//! "test@example.com",
|
||||
//! "test@example.org",
|
||||
//! "Hello world !"
|
||||
//! );
|
||||
//!
|
||||
//! let mut sender = SenderBuilder::localhost().unwrap().build();
|
||||
//! let result = sender.send(email);
|
||||
//! assert!(result.is_ok());
|
||||
//! ```
|
||||
//!
|
||||
//! ### Lower level
|
||||
//!
|
||||
//! You can also send commands, here is a simple email transaction without error handling:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use smtp::client::Client;
|
||||
//! use smtp::SMTP_PORT;
|
||||
//! use smtp::client::net::NetworkStream;
|
||||
//!
|
||||
//! let mut email_client: Client<NetworkStream> = Client::new();
|
||||
//! let _ = email_client.connect(&("localhost", SMTP_PORT));
|
||||
//! let _ = email_client.ehlo("my_hostname");
|
||||
//! let _ = email_client.mail("user@example.com", None);
|
||||
//! let _ = email_client.rcpt("user@example.org");
|
||||
//! let _ = email_client.data();
|
||||
//! let _ = email_client.message("Test email");
|
||||
//! let _ = email_client.quit();
|
||||
//! ```
|
||||
//! * **builder**: Message builder
|
||||
//! * **file-transport**: Transport that write messages into a file
|
||||
//! * **file-transport-envelope**: Allow writing the envelope into a JSON file
|
||||
//! * **smtp-transport**: Transport over SMTP
|
||||
//! * **sendmail-transport**: Transport over SMTP
|
||||
//! * **rustls-tls**: TLS support with the `rustls` crate
|
||||
//! * **native-tls**: TLS support with the `native-tls` crate
|
||||
//! * **tokio02**: Allow to asyncronously send emails using tokio 0.2.x
|
||||
//! * **tokio02-rustls-tls**: Async TLS support with the `rustls` crate using tokio 0.2
|
||||
//! * **tokio02-native-tls**: Async TLS support with the `native-tls` crate using tokio 0.2
|
||||
//! * **tokio1**: Allow to asyncronously send emails using tokio 1.x
|
||||
//! * **tokio1-rustls-tls**: Async TLS support with the `rustls` crate using tokio 1.x
|
||||
//! * **tokio1-native-tls**: Async TLS support with the `native-tls` crate 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 at the moment
|
||||
//! * **async-std1-rustls-tls**: Async TLS support with the `rustls` crate using async-std 1.x
|
||||
//! * **r2d2**: Connection pool for SMTP transport
|
||||
//! * **tracing**: Logging using the `tracing` crate
|
||||
//! * **serde**: Serialization/Deserialization of entities
|
||||
//! * **hostname**: Ability to try to use actual hostname in SMTP transaction
|
||||
|
||||
#![deny(missing_docs)]
|
||||
#![doc(html_root_url = "https://docs.rs/crate/lettre/0.10.0-alpha.5")]
|
||||
#![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(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unstable_features,
|
||||
unused_import_braces,
|
||||
rust_2018_idioms
|
||||
)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate rustc_serialize as serialize;
|
||||
extern crate crypto;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
extern crate email as email_format;
|
||||
extern crate bufstream;
|
||||
extern crate openssl;
|
||||
|
||||
mod extension;
|
||||
pub mod client;
|
||||
pub mod sender;
|
||||
pub mod response;
|
||||
pub mod address;
|
||||
pub mod error;
|
||||
pub mod authentication;
|
||||
pub mod email;
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
pub mod message;
|
||||
pub mod transport;
|
||||
|
||||
// Registrated port numbers:
|
||||
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
#[cfg(feature = "builder")]
|
||||
#[macro_use]
|
||||
extern crate hyperx;
|
||||
|
||||
/// Default smtp port
|
||||
pub static SMTP_PORT: u16 = 25;
|
||||
pub use crate::address::Address;
|
||||
use crate::address::Envelope;
|
||||
use crate::error::Error;
|
||||
#[cfg(feature = "builder")]
|
||||
pub use crate::message::Message;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use crate::transport::file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use crate::transport::sendmail::SendmailTransport;
|
||||
#[cfg(all(
|
||||
feature = "smtp-transport",
|
||||
any(feature = "tokio02", feature = "tokio1")
|
||||
))]
|
||||
pub use crate::transport::smtp::AsyncSmtpTransport;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "async-std1"))]
|
||||
pub use crate::transport::smtp::AsyncStd1Connector;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use crate::transport::smtp::SmtpTransport;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio02"))]
|
||||
pub use crate::transport::smtp::Tokio02Connector;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "tokio1"))]
|
||||
pub use crate::transport::smtp::Tokio1Connector;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Default smtps port
|
||||
pub static SMTPS_PORT: u16 = 465;
|
||||
/// Blocking Transport method for emails
|
||||
pub trait Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Default submission port
|
||||
pub static SUBMISSION_PORT: u16 = 587;
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
fn send(&self, message: &Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
self.send_raw(message.envelope(), &raw)
|
||||
}
|
||||
|
||||
// Useful strings and characters
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// The word separator for SMTP transactions
|
||||
pub static SP: &'static str = " ";
|
||||
/// async-std 1.x based Transport method for emails
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
#[async_trait]
|
||||
pub trait AsyncStd1Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// The line ending for SMTP transactions (carriage return + line feed)
|
||||
pub static CRLF: &'static str = "\r\n";
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
/// Colon
|
||||
pub static COLON: &'static str = ":";
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// The ending of message content
|
||||
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
|
||||
/// tokio 0.2.x based Transport method for emails
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
|
||||
#[async_trait]
|
||||
pub trait Tokio02Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// NUL unicode character
|
||||
pub static NUL: &'static str = "\0";
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
/// tokio 1.x based Transport method for emails
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
#[async_trait]
|
||||
pub trait Tokio1Transport {
|
||||
/// Response produced by the Transport
|
||||
type Ok;
|
||||
/// Error produced by the Transport
|
||||
type Error;
|
||||
|
||||
/// Sends the email
|
||||
#[cfg(feature = "builder")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "builder")))]
|
||||
// TODO take &Message
|
||||
async fn send(&self, message: Message) -> Result<Self::Ok, Self::Error> {
|
||||
let raw = message.formatted();
|
||||
let envelope = message.envelope();
|
||||
self.send_raw(&envelope, &raw).await
|
||||
}
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::{header, Mailbox, Mailboxes};
|
||||
use hyperx::header::Headers;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[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));
|
||||
headers.set(header::Sender(sender));
|
||||
headers.set(header::To(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));
|
||||
headers.set(header::Sender(sender));
|
||||
|
||||
assert!(Envelope::try_from(&headers).is_err(),);
|
||||
}
|
||||
}
|
||||
|
||||
579
src/message/body.rs
Normal file
579
src/message/body.rs
Normal file
@@ -0,0 +1,579 @@
|
||||
use std::io::{self, Write};
|
||||
use std::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(Vec<u8>),
|
||||
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 `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 buf: MaybeString = buf.into();
|
||||
|
||||
let encoding = buf.encoding();
|
||||
Self::new_impl(buf.into(), encoding)
|
||||
}
|
||||
|
||||
/// Encode the supplied `buf`, using the provided `encoding`.
|
||||
///
|
||||
/// [`Body::new`] is generally the better option.
|
||||
///
|
||||
/// 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 buf: MaybeString = buf.into();
|
||||
|
||||
if !buf.is_encoding_ok(encoding) {
|
||||
return Err(buf.into());
|
||||
}
|
||||
|
||||
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 base64_len = buf.len() * 4 / 3 + 4;
|
||||
let base64_endings_len = base64_len + base64_len / LINE_MAX_LENGTH;
|
||||
|
||||
let mut out = Vec::with_capacity(base64_endings_len);
|
||||
{
|
||||
let writer = LineWrappingWriter::new(&mut out, LINE_MAX_LENGTH);
|
||||
let mut writer = base64::write::EncoderWriter::new(writer, base64::STANDARD);
|
||||
|
||||
// TODO: use writer.write_all(self.as_ref()).expect("base64 encoding never fails");
|
||||
|
||||
// modified Write::write_all to work around base64 crate bug
|
||||
// TODO: remove once https://github.com/marshallpierce/rust-base64/issues/148 is fixed
|
||||
{
|
||||
let mut buf: &[u8] = buf.as_ref();
|
||||
while !buf.is_empty() {
|
||||
match writer.write(buf) {
|
||||
Ok(0) => {
|
||||
// ignore 0 writes
|
||||
}
|
||||
Ok(n) => {
|
||||
buf = &buf[n..];
|
||||
}
|
||||
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => {}
|
||||
Err(e) => panic!("base64 encoding never fails: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Self::dangerous_pre_encoded(out, 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`
|
||||
///
|
||||
/// If the `MaybeString` was created from a `String` composed only of US-ASCII
|
||||
/// characters, with no lines longer than 1000 characters, then 7bit
|
||||
/// encoding will be used, else quoted-printable will be chosen.
|
||||
///
|
||||
/// If the `MaybeString` was instead created from a `Vec<u8>`, base64 encoding is always
|
||||
/// chosen.
|
||||
///
|
||||
/// `8bit` and `binary` encodings are never returned, as they may not be
|
||||
/// supported by all SMTP servers.
|
||||
pub fn encoding(&self) -> ContentTransferEncoding {
|
||||
match &self {
|
||||
Self::String(s) if is_7bit_encoded(s.as_ref()) => ContentTransferEncoding::SevenBit,
|
||||
// TODO: consider when base64 would be a better option because of output size
|
||||
Self::String(_) => ContentTransferEncoding::QuotedPrintable,
|
||||
Self::Binary(_) => ContentTransferEncoding::Base64,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if using `encoding` to encode this `MaybeString`
|
||||
/// would result into an invalid encoded body.
|
||||
fn is_encoding_ok(&self, encoding: ContentTransferEncoding) -> bool {
|
||||
match encoding {
|
||||
ContentTransferEncoding::SevenBit => is_7bit_encoded(&self),
|
||||
ContentTransferEncoding::EightBit => is_8bit_encoded(&self),
|
||||
ContentTransferEncoding::Binary
|
||||
| ContentTransferEncoding::QuotedPrintable
|
||||
| ContentTransferEncoding::Base64 => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks whether it contains only US-ASCII characters,
|
||||
/// and no lines are longer than 1000 characters including the `\n` character.
|
||||
///
|
||||
/// Most efficient content encoding available
|
||||
fn is_7bit_encoded(buf: &[u8]) -> bool {
|
||||
buf.is_ascii() && !contains_too_long_lines(buf)
|
||||
}
|
||||
|
||||
/// Checks that no lines are longer than 1000 characters,
|
||||
/// including the `\n` character.
|
||||
/// NOTE: 8bit isn't supported by all SMTP servers.
|
||||
fn is_8bit_encoded(buf: &[u8]) -> bool {
|
||||
!contains_too_long_lines(buf)
|
||||
}
|
||||
|
||||
/// Checks if there are lines that are longer than 1000 characters,
|
||||
/// including the `\n` character.
|
||||
fn contains_too_long_lines(buf: &[u8]) -> bool {
|
||||
buf.len() > 1000 && buf.split(|&b| b == b'\n').any(|line| line.len() > 999)
|
||||
}
|
||||
|
||||
const LINE_SEPARATOR: &[u8] = b"\r\n";
|
||||
const LINE_MAX_LENGTH: usize = 78 - LINE_SEPARATOR.len();
|
||||
|
||||
/// A `Write`r that inserts a line separator `\r\n` every `max_line_length` bytes.
|
||||
struct LineWrappingWriter<'a, W> {
|
||||
writer: &'a mut W,
|
||||
current_line_length: usize,
|
||||
max_line_length: usize,
|
||||
}
|
||||
|
||||
impl<'a, W> LineWrappingWriter<'a, W> {
|
||||
pub fn new(writer: &'a mut W, max_line_length: usize) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
current_line_length: 0,
|
||||
max_line_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, W> Write for LineWrappingWriter<'a, W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let remaining_line_len = self.max_line_length - self.current_line_length;
|
||||
let write_len = std::cmp::min(buf.len(), remaining_line_len);
|
||||
|
||||
self.writer.write_all(&buf[..write_len])?;
|
||||
|
||||
if remaining_line_len == write_len {
|
||||
self.writer.write_all(LINE_SEPARATOR)?;
|
||||
|
||||
self.current_line_length = 0;
|
||||
} else {
|
||||
self.current_line_length += write_len;
|
||||
}
|
||||
|
||||
Ok(write_len)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{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("Привет, мир!"));
|
||||
|
||||
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_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("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::QuotedPrintable);
|
||||
assert_eq!(
|
||||
encoded.as_ref(),
|
||||
concat!(
|
||||
"=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"
|
||||
)
|
||||
.as_bytes()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_detect() {
|
||||
let input = Body::new(vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
let encoding = input.encoding();
|
||||
assert_eq!(encoding, ContentTransferEncoding::Base64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
|
||||
ContentTransferEncoding::Base64,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(encoded.encoding(), ContentTransferEncoding::Base64);
|
||||
assert_eq!(encoded.as_ref(), b"AAECAwQFBgcICQ==");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base64_encode_bytes_wrapping() {
|
||||
let encoded = Body::new_with_encoding(
|
||||
vec![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()
|
||||
);
|
||||
}
|
||||
}
|
||||
126
src/message/header/content.rs
Normal file
126
src/message/header/content.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter as FmtFormatter, Result as FmtResult},
|
||||
str::{from_utf8, FromStr},
|
||||
};
|
||||
|
||||
header! { (ContentId, "Content-ID") => [String] }
|
||||
|
||||
/// `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)]
|
||||
pub enum ContentTransferEncoding {
|
||||
SevenBit,
|
||||
QuotedPrintable,
|
||||
Base64,
|
||||
// 8BITMIME
|
||||
EightBit,
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Default for ContentTransferEncoding {
|
||||
fn default() -> Self {
|
||||
ContentTransferEncoding::Base64
|
||||
}
|
||||
}
|
||||
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for ContentTransferEncoding {
|
||||
fn header_name() -> &'static str {
|
||||
"Content-Transfer-Encoding"
|
||||
}
|
||||
|
||||
// FIXME HeaderError->HeaderError, same for result
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(|r| from_utf8(r).map_err(|_| HeaderError::Header))
|
||||
.and_then(|s| {
|
||||
s.parse::<ContentTransferEncoding>()
|
||||
.map_err(|_| HeaderError::Header)
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ContentTransferEncoding;
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(ContentTransferEncoding::SevenBit);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: 7bit\r\n"
|
||||
);
|
||||
|
||||
headers.set(ContentTransferEncoding::Base64);
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Content-Transfer-Encoding: base64\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_content_transfer_encoding() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "7bit");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::SevenBit)
|
||||
);
|
||||
|
||||
headers.set_raw("Content-Transfer-Encoding", "base64");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<ContentTransferEncoding>(),
|
||||
Some(&ContentTransferEncoding::Base64)
|
||||
);
|
||||
}
|
||||
}
|
||||
289
src/message/header/mailbox.rs
Normal file
289
src/message/header/mailbox.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
use crate::message::{
|
||||
mailbox::{Mailbox, Mailboxes},
|
||||
utf8_b,
|
||||
};
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, slice::Iter, str::from_utf8};
|
||||
|
||||
/// 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)]
|
||||
pub struct $type_name(pub Mailbox);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self> where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized {
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.and_then(|mbs| {
|
||||
mbs.into_single().ok_or(HeaderError::Header)
|
||||
}).map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&self.0.recode_name(utf8_b::encode))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mailboxes_header {
|
||||
($(#[$doc:meta])*($type_name: ident, $header_name: expr)) => {
|
||||
$(#[$doc])*
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub Mailboxes);
|
||||
|
||||
impl MailboxesHeader for $type_name {
|
||||
fn join_mailboxes(&mut self, other: Self) {
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_mailboxes)
|
||||
.map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
format_mailboxes(self.0.iter(), f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mailbox_header! {
|
||||
/**
|
||||
|
||||
`Sender` header
|
||||
|
||||
This header contains [`Mailbox`][self::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`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(From, "From")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Reply-To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(ReplyTo, "Reply-To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`To` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(To, "To")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Cc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(Cc, "Cc")
|
||||
}
|
||||
|
||||
mailboxes_header! {
|
||||
/**
|
||||
|
||||
`Bcc` header
|
||||
|
||||
This header contains [`Mailboxes`][self::Mailboxes].
|
||||
|
||||
*/
|
||||
(Bcc, "Bcc")
|
||||
}
|
||||
|
||||
fn parse_mailboxes(raw: &[u8]) -> HyperResult<Mailboxes> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Ok(mbs) = src.parse() {
|
||||
return Ok(mbs);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn format_mailboxes<'a>(mbs: Iter<'a, Mailbox>, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&Mailboxes::from(
|
||||
mbs.map(|mb| mb.recode_name(utf8_b::encode))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{From, Mailbox, Mailboxes};
|
||||
use hyperx::header::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!(format!("{}", headers), "From: kayo@example.com\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_single_with_name() {
|
||||
let from = Mailboxes::new().with("K. <kayo@example.com>".parse().unwrap());
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from));
|
||||
|
||||
assert_eq!(format!("{}", headers), "From: K. <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!(
|
||||
format!("{}", headers),
|
||||
"From: kayo@example.com, pony@domain.tld\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_multi_with_name() {
|
||||
let from = vec![
|
||||
"K. <kayo@example.com>".parse().unwrap(),
|
||||
"Pony P. <pony@domain.tld>".parse().unwrap(),
|
||||
];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set(From(from.into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"From: K. <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!(
|
||||
format!("{}", headers),
|
||||
"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.set_raw("From", "kayo@example.com");
|
||||
|
||||
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.set_raw("From", "K. <kayo@example.com>");
|
||||
|
||||
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.set_raw("From", "kayo@example.com, pony@domain.tld");
|
||||
|
||||
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.set_raw("From", "K. <kayo@example.com>, Pony P. <pony@domain.tld>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_single_with_utf8_name() {
|
||||
let from: Vec<Mailbox> = vec!["Кайо <kayo@example.com>".parse().unwrap()];
|
||||
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("From", "=?utf-8?b?0JrQsNC50L4=?= <kayo@example.com>");
|
||||
|
||||
assert_eq!(headers.get::<From>(), Some(&From(from.into())));
|
||||
}
|
||||
}
|
||||
17
src/message/header/mod.rs
Normal file
17
src/message/header/mod.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
/*!
|
||||
|
||||
## Headers widely used in email messages
|
||||
|
||||
*/
|
||||
|
||||
mod content;
|
||||
mod mailbox;
|
||||
mod special;
|
||||
mod textual;
|
||||
|
||||
pub use self::{content::*, mailbox::*, special::*, textual::*};
|
||||
|
||||
pub use hyperx::header::{
|
||||
Charset, ContentDisposition, ContentLocation, ContentType, Date, DispositionParam,
|
||||
DispositionType, Header, Headers, HttpDate as EmailDate,
|
||||
};
|
||||
83
src/message/header/special.rs
Normal file
83
src/message/header/special.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MimeVersion {
|
||||
pub major: u8,
|
||||
pub minor: u8,
|
||||
}
|
||||
|
||||
pub const MIME_VERSION_1_0: MimeVersion = MimeVersion { major: 1, minor: 0 };
|
||||
|
||||
impl MimeVersion {
|
||||
pub fn new(major: u8, minor: u8) -> Self {
|
||||
MimeVersion { major, minor }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MimeVersion {
|
||||
fn default() -> Self {
|
||||
MIME_VERSION_1_0
|
||||
}
|
||||
}
|
||||
|
||||
impl Header for MimeVersion {
|
||||
fn header_name() -> &'static str {
|
||||
"MIME-Version"
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<Self>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one().ok_or(HeaderError::Header).and_then(|r| {
|
||||
let mut s = from_utf8(r).map_err(|_| HeaderError::Header)?.split('.');
|
||||
|
||||
let major = s.next().ok_or(HeaderError::Header)?;
|
||||
let minor = s.next().ok_or(HeaderError::Header)?;
|
||||
let major = major.parse().map_err(|_| HeaderError::Header)?;
|
||||
let minor = minor.parse().map_err(|_| HeaderError::Header)?;
|
||||
Ok(MimeVersion::new(major, minor))
|
||||
})
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&format!("{}.{}", self.major, self.minor))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{MimeVersion, MIME_VERSION_1_0};
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set(MIME_VERSION_1_0);
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 1.0\r\n");
|
||||
|
||||
headers.set(MimeVersion::new(0, 1));
|
||||
|
||||
assert_eq!(format!("{}", headers), "MIME-Version: 0.1\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_mime_version() {
|
||||
let mut headers = Headers::new();
|
||||
|
||||
headers.set_raw("MIME-Version", "1.0");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MIME_VERSION_1_0));
|
||||
|
||||
headers.set_raw("MIME-Version", "0.1");
|
||||
|
||||
assert_eq!(headers.get::<MimeVersion>(), Some(&MimeVersion::new(0, 1)));
|
||||
}
|
||||
}
|
||||
105
src/message/header/textual.rs
Normal file
105
src/message/header/textual.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
use crate::message::utf8_b;
|
||||
use hyperx::{
|
||||
header::{Formatter as HeaderFormatter, Header, RawLike},
|
||||
Error as HeaderError, Result as HyperResult,
|
||||
};
|
||||
use std::{fmt::Result as FmtResult, str::from_utf8};
|
||||
|
||||
macro_rules! text_header {
|
||||
( $type_name: ident, $header_name: expr ) => {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct $type_name(pub String);
|
||||
|
||||
impl Header for $type_name {
|
||||
fn header_name() -> &'static str {
|
||||
$header_name
|
||||
}
|
||||
|
||||
fn parse_header<'a, T>(raw: &'a T) -> HyperResult<$type_name>
|
||||
where
|
||||
T: RawLike<'a>,
|
||||
Self: Sized,
|
||||
{
|
||||
raw.one()
|
||||
.ok_or(HeaderError::Header)
|
||||
.and_then(parse_text)
|
||||
.map($type_name)
|
||||
}
|
||||
|
||||
fn fmt_header(&self, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
fmt_text(&self.0, f)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
text_header!(Subject, "Subject");
|
||||
text_header!(Comments, "Comments");
|
||||
text_header!(Keywords, "Keywords");
|
||||
text_header!(InReplyTo, "In-Reply-To");
|
||||
text_header!(References, "References");
|
||||
text_header!(MessageId, "Message-Id");
|
||||
text_header!(UserAgent, "User-Agent");
|
||||
|
||||
fn parse_text(raw: &[u8]) -> HyperResult<String> {
|
||||
if let Ok(src) = from_utf8(raw) {
|
||||
if let Some(txt) = utf8_b::decode(src) {
|
||||
return Ok(txt);
|
||||
}
|
||||
}
|
||||
Err(HeaderError::Header)
|
||||
}
|
||||
|
||||
fn fmt_text(s: &str, f: &mut HeaderFormatter<'_, '_>) -> FmtResult {
|
||||
f.fmt_line(&utf8_b::encode(s))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Subject;
|
||||
use hyperx::header::Headers;
|
||||
|
||||
#[test]
|
||||
fn format_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Sample subject".into()));
|
||||
|
||||
assert_eq!(format!("{}", headers), "Subject: Sample subject\r\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set(Subject("Тема сообщения".into()));
|
||||
|
||||
assert_eq!(
|
||||
format!("{}", headers),
|
||||
"Subject: =?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=\r\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_ascii() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw("Subject", "Sample subject");
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Sample subject".into()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_utf8() {
|
||||
let mut headers = Headers::new();
|
||||
headers.set_raw(
|
||||
"Subject",
|
||||
"=?utf-8?b?0KLQtdC80LAg0YHQvtC+0LHRidC10L3QuNGP?=",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
headers.get::<Subject>(),
|
||||
Some(&Subject("Тема сообщения".into()))
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/message/mailbox/mod.rs
Normal file
5
src/message/mailbox/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
mod types;
|
||||
|
||||
pub use self::types::*;
|
||||
215
src/message/mailbox/serde.rs
Normal file
215
src/message/mailbox/serde.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use crate::message::{Mailbox, Mailboxes};
|
||||
use serde::{
|
||||
de::{Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor},
|
||||
ser::Serializer,
|
||||
Deserialize, Serialize,
|
||||
};
|
||||
use std::fmt::{Formatter, Result as FmtResult};
|
||||
|
||||
impl Serialize for Mailbox {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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<'de> Visitor<'de> 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.serialize_str(&self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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 super::*;
|
||||
use crate::address::Address;
|
||||
use serde_json::from_str;
|
||||
|
||||
#[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_stirng() {
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
435
src/message/mailbox/types.rs
Normal file
435
src/message/mailbox/types.rs
Normal file
@@ -0,0 +1,435 @@
|
||||
use crate::{
|
||||
address::{Address, AddressError},
|
||||
message::utf8_b,
|
||||
};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
fmt::{Display, Formatter, Result as FmtResult, Write},
|
||||
slice::Iter,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// 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 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::{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(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn new(name: Option<String>, email: Address) -> Self {
|
||||
Mailbox { name, email }
|
||||
}
|
||||
|
||||
/// Encode addressee name using function
|
||||
pub(crate) fn recode_name<F>(&self, f: F) -> Self
|
||||
where
|
||||
F: FnOnce(&str) -> String,
|
||||
{
|
||||
Mailbox::new(self.name.clone().map(|s| f(&s)), self.email.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Mailbox {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
if let Some(ref name) = self.name {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
f.write_str(&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<S: AsRef<&str>, T: AsRef<&str>> 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.as_ref()), address.as_ref().parse()?))
|
||||
}
|
||||
}*/
|
||||
|
||||
impl FromStr for Mailbox {
|
||||
type Err = AddressError;
|
||||
|
||||
fn from_str(src: &str) -> Result<Mailbox, Self::Err> {
|
||||
match (src.find('<'), src.find('>')) {
|
||||
(Some(addr_open), Some(addr_close)) if addr_open < addr_close => {
|
||||
let name = src.split_at(addr_open).0;
|
||||
let addr_open = addr_open + 1;
|
||||
let addr = src.split_at(addr_open).1.split_at(addr_close - addr_open).0;
|
||||
let addr = addr.parse()?;
|
||||
let name = name.trim();
|
||||
let name = if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(name.into())
|
||||
};
|
||||
Ok(Mailbox::new(name, addr))
|
||||
}
|
||||
(Some(_), _) => Err(AddressError::Unbalanced),
|
||||
_ => {
|
||||
let addr = src.parse()?;
|
||||
Ok(Mailbox::new(None, addr))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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::{Address, message::{Mailbox, Mailboxes}};
|
||||
///
|
||||
/// # 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::{Address, message::{Mailbox, Mailboxes}};
|
||||
///
|
||||
/// # 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::{Address, message::{Mailbox, Mailboxes}};
|
||||
///
|
||||
/// # 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::{Address, message::{Mailbox, Mailboxes}};
|
||||
///
|
||||
/// # 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()
|
||||
}
|
||||
}
|
||||
|
||||
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 IntoIterator for Mailboxes {
|
||||
type Item = Mailbox;
|
||||
type IntoIter = ::std::vec::IntoIter<Mailbox>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.0.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl Extend<Mailbox> for Mailboxes {
|
||||
fn extend<T: IntoIterator<Item = Mailbox>>(&mut self, iter: T) {
|
||||
for elem in iter {
|
||||
self.0.push(elem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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> {
|
||||
src.split(',')
|
||||
.map(|m| {
|
||||
m.trim().parse().and_then(|Mailbox { name, email }| {
|
||||
if let Some(name) = name {
|
||||
if let Some(name) = utf8_b::decode(&name) {
|
||||
Ok(Mailbox::new(Some(name), email))
|
||||
} else {
|
||||
Err(AddressError::InvalidUtf8b)
|
||||
}
|
||||
} else {
|
||||
Ok(Mailbox::new(None, email))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(Mailboxes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Mailbox;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[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 format_address_with_empty_name() {
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mailbox::new(Some("".into()), "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_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_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_string(), "kayo@example.com".to_string()).try_into(),
|
||||
Ok(Mailbox::new(
|
||||
Some("K.".into()),
|
||||
"kayo@example.com".parse().unwrap()
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
779
src/message/mimebody.rs
Normal file
779
src/message/mimebody.rs
Normal file
@@ -0,0 +1,779 @@
|
||||
use crate::message::{
|
||||
header::{ContentTransferEncoding, ContentType, Header, Headers},
|
||||
EmailFormat, IntoBody,
|
||||
};
|
||||
use mime::Mime;
|
||||
use rand::Rng;
|
||||
|
||||
/// MIME part variants
|
||||
///
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Part {
|
||||
/// Single part with content
|
||||
///
|
||||
Single(SinglePart),
|
||||
|
||||
/// Multiple parts of content
|
||||
///
|
||||
Multi(MultiPart),
|
||||
}
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Part {
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
/// Parts of multipart body
|
||||
///
|
||||
pub type Parts = Vec<Part>;
|
||||
|
||||
/// 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>().copied();
|
||||
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::{SinglePart, header};
|
||||
///
|
||||
/// # use std::error::Error;
|
||||
/// # fn main() -> Result<(), Box<dyn Error>> {
|
||||
/// let part = SinglePart::builder()
|
||||
/// .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
/// .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()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
|
||||
pub fn seven_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::SevenBit)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
|
||||
pub fn quoted_printable() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::QuotedPrintable)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
|
||||
pub fn base64() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::Base64)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated = "Replaced by SinglePart::builder(), which chooses the best Content-Transfer-Encoding based on the provided body"]
|
||||
pub fn eight_bit() -> SinglePartBuilder {
|
||||
Self::builder().header(ContentTransferEncoding::EightBit)
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for SinglePart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
out.extend_from_slice(&self.body);
|
||||
out.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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 single email message.
|
||||
Alternative,
|
||||
|
||||
/// Related kind to mix content and related resources.
|
||||
///
|
||||
/// For example, you can include images into 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.
|
||||
fn make_boundary() -> String {
|
||||
rand::thread_rng()
|
||||
.sample_iter(rand::distributions::Alphanumeric)
|
||||
.take(40)
|
||||
.map(char::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl MultiPartKind {
|
||||
fn to_mime<S: Into<String>>(&self, boundary: Option<S>) -> Mime {
|
||||
let boundary = boundary.map_or_else(make_boundary, |s| s.into());
|
||||
|
||||
use self::MultiPartKind::*;
|
||||
format!(
|
||||
"multipart/{}; boundary=\"{}\"{}",
|
||||
match self {
|
||||
Mixed => "mixed",
|
||||
Alternative => "alternative",
|
||||
Related => "related",
|
||||
Encrypted { .. } => "encrypted",
|
||||
Signed { .. } => "signed",
|
||||
},
|
||||
boundary,
|
||||
match self {
|
||||
Encrypted { protocol } => format!("; protocol=\"{}\"", protocol),
|
||||
Signed { protocol, micalg } =>
|
||||
format!("; protocol=\"{}\"; micalg=\"{}\"", protocol, micalg),
|
||||
_ => String::new(),
|
||||
}
|
||||
)
|
||||
.parse()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn from_mime(m: &Mime) -> Option<Self> {
|
||||
use self::MultiPartKind::*;
|
||||
match m.subtype().as_ref() {
|
||||
"mixed" => Some(Mixed),
|
||||
"alternative" => Some(Alternative),
|
||||
"related" => Some(Related),
|
||||
"signed" => m.get_param("protocol").and_then(|p| {
|
||||
m.get_param("micalg").map(|micalg| Signed {
|
||||
protocol: p.as_str().to_owned(),
|
||||
micalg: micalg.as_str().to_owned(),
|
||||
})
|
||||
}),
|
||||
"encrypted" => m.get_param("protocol").map(|p| Encrypted {
|
||||
protocol: p.as_str().to_owned(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MultiPartKind> for Mime {
|
||||
fn from(m: MultiPartKind) -> Self {
|
||||
m.to_mime::<String>(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(kind.into()))
|
||||
}
|
||||
|
||||
/// Set custom boundary
|
||||
pub fn boundary<S: AsRef<str>>(self, boundary: S) -> Self {
|
||||
let kind = {
|
||||
let mime = &self.headers.get::<ContentType>().unwrap().0;
|
||||
MultiPartKind::from_mime(mime).unwrap()
|
||||
};
|
||||
let mime = kind.to_mime(Some(boundary.as_ref()));
|
||||
self.header(ContentType(mime))
|
||||
}
|
||||
|
||||
/// Creates multipart without parts
|
||||
pub fn build(self) -> MultiPart {
|
||||
MultiPart {
|
||||
headers: self.headers,
|
||||
parts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates multipart using part
|
||||
pub fn part(self, part: Part) -> MultiPart {
|
||||
self.build().part(part)
|
||||
}
|
||||
|
||||
/// 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: Parts,
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
/// Add part to multipart
|
||||
pub fn part(mut self, part: Part) -> Self {
|
||||
self.parts.push(part);
|
||||
self
|
||||
}
|
||||
|
||||
/// 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().0;
|
||||
content_type.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 the parts from the multipart
|
||||
pub fn parts(&self) -> &Parts {
|
||||
&self.parts
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the parts
|
||||
pub fn parts_mut(&mut self) -> &mut Parts {
|
||||
&mut self.parts
|
||||
}
|
||||
|
||||
/// Get message content formatted for SMTP
|
||||
pub fn formatted(&self) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
self.format(&mut out);
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for MultiPart {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
out.extend_from_slice(b"\r\n");
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::message::header;
|
||||
|
||||
#[test]
|
||||
fn single_part_binary() {
|
||||
let part = SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::QuotedPrintable)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("Текст письма в уникоде"));
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: base64\r\n",
|
||||
"\r\n",
|
||||
"0KLQtdC60YHRgiDQv9C40YHRjNC80LAg0LIg0YPQvdC40LrQvtC00LU=\r\n"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"text/plain; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"example.c".into(),
|
||||
)],
|
||||
})
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")),
|
||||
);
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_encrypted() {
|
||||
let part = MultiPart::encrypted("application/pgp-encrypted".to_owned())
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType(
|
||||
"application/pgp-encrypted".parse().unwrap(),
|
||||
))
|
||||
.body(String::from("Version: 1")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/octet-stream; name=\"encrypted.asc\""
|
||||
.parse()
|
||||
.unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"encrypted.asc".into(),
|
||||
)],
|
||||
})
|
||||
.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;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
" protocol=\"application/pgp-encrypted\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: application/pgp-encrypted\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Version: 1\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
#[test]
|
||||
fn multi_part_signed() {
|
||||
let part = MultiPart::signed(
|
||||
"application/pgp-signature".to_owned(),
|
||||
"pgp-sha256".to_owned(),
|
||||
)
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType("text/plain".parse().unwrap()))
|
||||
.body(String::from("Test email for signature")),
|
||||
))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType(
|
||||
"application/pgp-signature; name=\"signature.asc\""
|
||||
.parse()
|
||||
.unwrap(),
|
||||
))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(
|
||||
header::Charset::Ext("utf-8".into()),
|
||||
None,
|
||||
"signature.asc".into(),
|
||||
)],
|
||||
})
|
||||
.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;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\";",
|
||||
" protocol=\"application/pgp-signature\";",
|
||||
" micalg=\"pgp-sha256\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Test email for signature\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\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",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_alternative() {
|
||||
let part = MultiPart::alternative()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.part(Part::Single(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("Текст письма в уникоде"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.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;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"Текст письма в уникоде\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_part_mixed_related() {
|
||||
let part = MultiPart::mixed()
|
||||
.boundary("F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK")
|
||||
.multipart(MultiPart::related()
|
||||
.boundary("E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh")
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/html; charset=utf8".parse().unwrap()))
|
||||
.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("image/png".parse().unwrap()))
|
||||
.header(header::ContentLocation("/image.png".into()))
|
||||
.header(header::ContentTransferEncoding::Base64)
|
||||
.body(String::from("1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"))))
|
||||
.singlepart(SinglePart::builder()
|
||||
.header(header::ContentType("text/plain; charset=utf8".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Attachment,
|
||||
parameters: vec![header::DispositionParam::Filename(header::Charset::Ext("utf-8".into()), None, "example.c".into())]
|
||||
})
|
||||
.header(header::ContentTransferEncoding::Binary)
|
||||
.body(String::from("int main() { return 0; }")));
|
||||
|
||||
assert_eq!(String::from_utf8(part.formatted()).unwrap(),
|
||||
concat!("Content-Type: multipart/mixed;",
|
||||
" boundary=\"F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\"\r\n",
|
||||
"\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: multipart/related;",
|
||||
" boundary=\"E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\"\r\n",
|
||||
"\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\r\n",
|
||||
"Content-Type: text/html; charset=utf8\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"<p>Текст <em>письма</em> в <a href=\"https://ru.wikipedia.org/wiki/Юникод\">уникоде</a><p>\r\n",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh\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",
|
||||
"--E912L4JH3loAAAAAFu/33Gx7PEoTMmhGaxG3FlbVMQHctj96q4nHvBM+7DTtXo/im8gh--\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK\r\n",
|
||||
"Content-Type: text/plain; charset=utf8\r\n",
|
||||
"Content-Disposition: attachment; filename=\"example.c\"\r\n",
|
||||
"Content-Transfer-Encoding: binary\r\n",
|
||||
"\r\n",
|
||||
"int main() { return 0; }\r\n",
|
||||
"--F2mTKN843loAAAAA8porEdAjCKhArPxGeahYoZYSftse1GT/84tup+O0bs8eueVuAlMK--\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());
|
||||
}
|
||||
}
|
||||
}
|
||||
663
src/message/mod.rs
Normal file
663
src/message/mod.rs
Normal file
@@ -0,0 +1,663 @@
|
||||
//! Provides a strongly typed way to build emails
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! This section demonstrates how to build messages.
|
||||
//!
|
||||
//! <!--
|
||||
//! style for <details><summary>Blablabla</summary> Lots of stuff</details>
|
||||
//! borrowed from https://docs.rs/time/0.2.23/src/time/lib.rs.html#49-54
|
||||
//! -->
|
||||
//! <style>
|
||||
//! summary, details:not([open]) { cursor: pointer; }
|
||||
//! summary { display: list-item; }
|
||||
//! summary::marker { content: '▶ '; }
|
||||
//! details[open] summary::marker { content: '▼ '; }
|
||||
//! </style>
|
||||
//!
|
||||
//!
|
||||
//! ### Plain body
|
||||
//!
|
||||
//! The easiest way of creating a message, which uses a plain text body.
|
||||
//!
|
||||
//! ```rust
|
||||
//! use lettre::message::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")
|
||||
//! .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-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, Part, 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()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .body(String::from("Hello, world! :)")),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/html; charset=utf8".parse()?))
|
||||
//! .body(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::{Body, header, Message, MultiPart, Part, 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::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .body(String::from("Hello, world! :)")),
|
||||
//! )
|
||||
//! .multipart(
|
||||
//! MultiPart::related()
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType(
|
||||
//! "text/html; charset=utf8".parse()?,
|
||||
//! ))
|
||||
//! .body(String::from(
|
||||
//! "<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
//! )),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("image/png".parse()?))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Inline,
|
||||
//! parameters: vec![],
|
||||
//! })
|
||||
//! .header(header::ContentId("<123>".into()))
|
||||
//! .body(image_body),
|
||||
//! ),
|
||||
//! ),
|
||||
//! )
|
||||
//! .singlepart(
|
||||
//! SinglePart::builder()
|
||||
//! .header(header::ContentType("text/plain; charset=utf8".parse()?))
|
||||
//! .header(header::ContentDisposition {
|
||||
//! disposition: header::DispositionType::Attachment,
|
||||
//! parameters: vec![header::DispositionParam::Filename(
|
||||
//! header::Charset::Ext("utf-8".into()),
|
||||
//! None,
|
||||
//! "example.rs".as_bytes().into(),
|
||||
//! )],
|
||||
//! })
|
||||
//! .body(String::from("fn main() { println!(\"Hello, World!\") }")),
|
||||
//! ),
|
||||
//! )?;
|
||||
//! # 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>
|
||||
|
||||
pub use body::{Body, IntoBody, MaybeString};
|
||||
pub use mailbox::*;
|
||||
pub use mimebody::*;
|
||||
|
||||
pub use mime;
|
||||
|
||||
mod body;
|
||||
pub mod header;
|
||||
mod mailbox;
|
||||
mod mimebody;
|
||||
mod utf8_b;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
address::Envelope,
|
||||
message::header::{ContentTransferEncoding, EmailDate, Header, Headers, MailboxesHeader},
|
||||
Error as EmailError,
|
||||
};
|
||||
|
||||
const DEFAULT_MESSAGE_ID_DOMAIN: &str = "localhost";
|
||||
|
||||
pub 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>,
|
||||
}
|
||||
|
||||
impl MessageBuilder {
|
||||
/// Creates a new default message builder
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
headers: Headers::new(),
|
||||
envelope: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>(mut self, header: H) -> Self {
|
||||
if self.headers.has::<H>() {
|
||||
self.headers.get_mut::<H>().unwrap().join_mailboxes(header);
|
||||
self
|
||||
} else {
|
||||
self.header(header)
|
||||
}
|
||||
}
|
||||
|
||||
/// Add `Date` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Date(date))`.
|
||||
pub fn date(self, date: EmailDate) -> Self {
|
||||
self.header(header::Date(date))
|
||||
}
|
||||
|
||||
/// Set `Date` header using current date/time
|
||||
///
|
||||
/// Shortcut for `self.date(SystemTime::now())`.
|
||||
pub fn date_now(self) -> Self {
|
||||
self.date(SystemTime::now().into())
|
||||
}
|
||||
|
||||
/// Set `Subject` header to message
|
||||
///
|
||||
/// Shortcut for `self.header(header::Subject(subject.into()))`.
|
||||
pub fn subject<S: Into<String>>(self, subject: S) -> Self {
|
||||
self.header(header::Subject(subject.into()))
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Set `Sender` header. Should be used when providing several `From` mailboxes.
|
||||
///
|
||||
/// 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(mbox))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `From` header
|
||||
///
|
||||
/// 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(mbox.into()))
|
||||
}
|
||||
|
||||
/// Set or add mailbox to `ReplyTo` header
|
||||
///
|
||||
/// 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(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(id))
|
||||
}
|
||||
|
||||
/// 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(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_string());
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
let hostname = DEFAULT_MESSAGE_ID_DOMAIN.to_string();
|
||||
|
||||
self.header(header::MessageId(
|
||||
// https://tools.ietf.org/html/rfc5322#section-3.6.4
|
||||
format!("<{}@{}>", Uuid::new_v4(), hostname),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set [User-Agent
|
||||
/// header](https://tools.ietf.org/html/draft-melnikov-email-user-agent-004)
|
||||
pub fn user_agent(self, id: String) -> Self {
|
||||
self.header(header::UserAgent(id))
|
||||
}
|
||||
|
||||
/// Force specific envelope (by default it is derived from headers)
|
||||
pub fn envelope(mut self, envelope: Envelope) -> Self {
|
||||
self.envelope = Some(envelope);
|
||||
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 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.clone().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)?,
|
||||
};
|
||||
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>().copied();
|
||||
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`][self::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`][self::SinglePart])
|
||||
pub fn singlepart(self, part: SinglePart) -> Result<Message, EmailError> {
|
||||
self.mime_1_0().build(MessageBody::Mime(Part::Single(part)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Email message which can be formatted
|
||||
#[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 `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
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailFormat for Message {
|
||||
fn format(&self, out: &mut Vec<u8>) {
|
||||
out.extend_from_slice(self.headers.to_string().as_bytes());
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::message::{header, mailbox::Mailbox, Message, MultiPart, SinglePart};
|
||||
|
||||
#[test]
|
||||
fn email_missing_originator() {
|
||||
assert!(Message::builder()
|
||||
.body(String::from("Happy new year!"))
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn email_miminal_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() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
|
||||
let email = Message::builder()
|
||||
.date(date)
|
||||
.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("яңа ел белән!".into()))
|
||||
.body(String::from("Happy new year!"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(email.formatted()).unwrap(),
|
||||
concat!(
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\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() {
|
||||
let date = "Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap();
|
||||
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; charset=utf8".parse().unwrap(),
|
||||
))
|
||||
.body(String::from(
|
||||
"<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>",
|
||||
)),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(header::ContentType("image/png".parse().unwrap()))
|
||||
.header(header::ContentDisposition {
|
||||
disposition: header::DispositionType::Inline,
|
||||
parameters: vec![],
|
||||
})
|
||||
.header(header::ContentId("<123>".into()))
|
||||
.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 == 6 || i == 8 || i == 13 || i == 232 {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert_eq!(line.0, line.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/message/utf8_b.rs
Normal file
60
src/message/utf8_b.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// https://tools.ietf.org/html/rfc1522
|
||||
|
||||
fn allowed_char(c: char) -> bool {
|
||||
c >= 1 as char && c <= 9 as char
|
||||
|| c == 11 as char
|
||||
|| c == 12 as char
|
||||
|| c >= 14 as char && c <= 127 as char
|
||||
}
|
||||
|
||||
pub fn encode(s: &str) -> String {
|
||||
if s.chars().all(allowed_char) {
|
||||
s.into()
|
||||
} else {
|
||||
format!("=?utf-8?b?{}?=", base64::encode(s))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode(s: &str) -> Option<String> {
|
||||
s.strip_prefix("=?utf-8?b?")
|
||||
.and_then(|stripped| stripped.strip_suffix("?="))
|
||||
.map_or_else(
|
||||
|| Some(s.into()),
|
||||
|stripped| {
|
||||
let decoded = base64::decode(stripped).ok()?;
|
||||
let decoded = String::from_utf8(decoded).ok()?;
|
||||
Some(decoded)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{decode, encode};
|
||||
|
||||
#[test]
|
||||
fn encode_ascii() {
|
||||
assert_eq!(&encode("Kayo. ?"), "Kayo. ?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_ascii() {
|
||||
assert_eq!(decode("Kayo. ?"), Some("Kayo. ?".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_utf8() {
|
||||
assert_eq!(
|
||||
&encode("Привет, мир!"),
|
||||
"=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_utf8() {
|
||||
assert_eq!(
|
||||
decode("=?utf-8?b?0J/RgNC40LLQtdGCLCDQvNC40YAh?="),
|
||||
Some("Привет, мир!".into())
|
||||
);
|
||||
}
|
||||
}
|
||||
588
src/response.rs
588
src/response.rs
@@ -1,588 +0,0 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text message
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
|
||||
use self::Severity::*;
|
||||
use self::Category::*;
|
||||
use error::{SmtpResult, Error};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
||||
pub enum Severity {
|
||||
/// 2yx
|
||||
PositiveCompletion,
|
||||
/// 3yz
|
||||
PositiveIntermediate,
|
||||
/// 4yz
|
||||
TransientNegativeCompletion,
|
||||
/// 5yz
|
||||
PermanentNegativeCompletion,
|
||||
}
|
||||
|
||||
impl FromStr for Severity {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> result::Result<Severity, Error> {
|
||||
match s {
|
||||
"2" => Ok(PositiveCompletion),
|
||||
"3" => Ok(PositiveIntermediate),
|
||||
"4" => Ok(TransientNegativeCompletion),
|
||||
"5" => Ok(PermanentNegativeCompletion),
|
||||
_ => Err(Error::ResponseParsingError("First digit must be between 2 and 5")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Severity {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
PositiveCompletion => 2,
|
||||
PositiveIntermediate => 3,
|
||||
TransientNegativeCompletion => 4,
|
||||
PermanentNegativeCompletion => 5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Second digit
|
||||
#[derive(PartialEq,Eq,Copy,Clone,Debug)]
|
||||
pub enum Category {
|
||||
/// x0z
|
||||
Syntax,
|
||||
/// x1z
|
||||
Information,
|
||||
/// x2z
|
||||
Connections,
|
||||
/// x3z
|
||||
Unspecified3,
|
||||
/// x4z
|
||||
Unspecified4,
|
||||
/// x5z
|
||||
MailSystem,
|
||||
}
|
||||
|
||||
impl FromStr for Category {
|
||||
type Err = Error;
|
||||
fn from_str(s: &str) -> result::Result<Category, Error> {
|
||||
match s {
|
||||
"0" => Ok(Syntax),
|
||||
"1" => Ok(Information),
|
||||
"2" => Ok(Connections),
|
||||
"3" => Ok(Unspecified3),
|
||||
"4" => Ok(Unspecified4),
|
||||
"5" => Ok(MailSystem),
|
||||
_ => Err(Error::ResponseParsingError("Second digit must be between 0 and 5")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Category {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write!(f,
|
||||
"{}",
|
||||
match *self {
|
||||
Syntax => 0,
|
||||
Information => 1,
|
||||
Connections => 2,
|
||||
Unspecified3 => 3,
|
||||
Unspecified4 => 4,
|
||||
MailSystem => 5,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 3 digit SMTP response code
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Code {
|
||||
/// First digit of the response code
|
||||
severity: Severity,
|
||||
/// Second digit of the response code
|
||||
category: Category,
|
||||
/// Third digit
|
||||
detail: u8,
|
||||
}
|
||||
|
||||
impl FromStr for Code {
|
||||
type Err = Error;
|
||||
|
||||
#[inline]
|
||||
fn from_str(s: &str) -> result::Result<Code, Error> {
|
||||
if s.len() == 3 {
|
||||
match (s[0..1].parse::<Severity>(),
|
||||
s[1..2].parse::<Category>(),
|
||||
s[2..3].parse::<u8>()) {
|
||||
(Ok(severity), Ok(category), Ok(detail)) => Ok(Code {
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
}),
|
||||
_ => return Err(Error::ResponseParsingError("Could not parse response code")),
|
||||
}
|
||||
} else {
|
||||
Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Code {
|
||||
/// Creates a new `Code` structure
|
||||
pub fn new(severity: Severity, category: Category, detail: u8) -> Code {
|
||||
Code {
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the reply code
|
||||
pub fn code(&self) -> String {
|
||||
format!("{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses an SMTP response
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct ResponseParser {
|
||||
/// Response code
|
||||
code: Option<Code>,
|
||||
/// Server response string (optional)
|
||||
/// Handle multiline responses
|
||||
message: Vec<String>,
|
||||
}
|
||||
|
||||
impl ResponseParser {
|
||||
/// Creates a new parser
|
||||
pub fn new() -> ResponseParser {
|
||||
ResponseParser {
|
||||
code: None,
|
||||
message: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a line and return a `bool` indicating if there are more lines to come
|
||||
pub fn read_line(&mut self, line: &str) -> result::Result<bool, Error> {
|
||||
|
||||
if line.len() < 3 {
|
||||
return Err(Error::ResponseParsingError("Wrong code length (should be 3 digit)"));
|
||||
}
|
||||
|
||||
match self.code {
|
||||
Some(ref code) => {
|
||||
if code.code() != line[0..3] {
|
||||
return Err(Error::ResponseParsingError("Response code has changed during a \
|
||||
reponse"));
|
||||
}
|
||||
}
|
||||
None => self.code = Some(try!(line[0..3].parse::<Code>())),
|
||||
}
|
||||
|
||||
if line.len() > 4 {
|
||||
self.message.push(line[4..].to_string());
|
||||
if line.as_bytes()[3] == '-' as u8 {
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds a response from a `ResponseParser`
|
||||
pub fn response(self) -> SmtpResult {
|
||||
match self.code {
|
||||
Some(code) => Ok(Response::new(code, self.message)),
|
||||
None => Err(Error::ResponseParsingError("Incomplete response, could not read \
|
||||
response code")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separed code and message
|
||||
///
|
||||
/// The text message is optional, only the code is mandatory
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Response {
|
||||
/// Response code
|
||||
code: Code,
|
||||
/// Server response string (optional)
|
||||
/// Handle multiline responses
|
||||
message: Vec<String>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response`
|
||||
pub fn new(code: Code, message: Vec<String>) -> Response {
|
||||
Response {
|
||||
code: code,
|
||||
message: message,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(&self) -> bool {
|
||||
match self.code.severity {
|
||||
PositiveCompletion => true,
|
||||
PositiveIntermediate => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the message
|
||||
pub fn message(&self) -> Vec<String> {
|
||||
self.message.clone()
|
||||
}
|
||||
|
||||
/// Returns the severity (i.e. 1st digit)
|
||||
pub fn severity(&self) -> Severity {
|
||||
self.code.severity
|
||||
}
|
||||
|
||||
/// Returns the category (i.e. 2nd digit)
|
||||
pub fn category(&self) -> Category {
|
||||
self.code.category
|
||||
}
|
||||
|
||||
/// Returns the detail (i.e. 3rd digit)
|
||||
pub fn detail(&self) -> u8 {
|
||||
self.code.detail
|
||||
}
|
||||
|
||||
/// Returns the reply code
|
||||
fn code(&self) -> String {
|
||||
self.code.code()
|
||||
}
|
||||
|
||||
/// Tests code equality
|
||||
pub fn has_code(&self, code: u16) -> bool {
|
||||
self.code() == format!("{}", code)
|
||||
}
|
||||
|
||||
/// Returns only the first word of the message if possible
|
||||
pub fn first_word(&self) -> Option<String> {
|
||||
match self.message.is_empty() {
|
||||
true => None,
|
||||
false => match self.message[0].split_whitespace().next() {
|
||||
Some(word) => Some(word.to_string()),
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Severity, Category, Response, ResponseParser, Code};
|
||||
|
||||
#[test]
|
||||
fn test_severity_from_str() {
|
||||
assert_eq!("2".parse::<Severity>().unwrap(),
|
||||
Severity::PositiveCompletion);
|
||||
assert_eq!("4".parse::<Severity>().unwrap(),
|
||||
Severity::TransientNegativeCompletion);
|
||||
assert!("1".parse::<Severity>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_severity_fmt() {
|
||||
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_from_str() {
|
||||
assert_eq!("2".parse::<Category>().unwrap(), Category::Connections);
|
||||
assert_eq!("4".parse::<Category>().unwrap(), Category::Unspecified4);
|
||||
assert!("6".parse::<Category>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_fmt() {
|
||||
assert_eq!(format!("{}", Category::Unspecified4), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_new() {
|
||||
assert_eq!(Code::new(Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
0),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 0,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_from_str() {
|
||||
assert_eq!("421".parse::<Code>().unwrap(),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 1,
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_code() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: 1,
|
||||
};
|
||||
|
||||
assert_eq!(code.code(), "421");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_new() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()]),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()],
|
||||
});
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![]),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
},
|
||||
message: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_parser() {
|
||||
let mut parser = ResponseParser::new();
|
||||
|
||||
assert!(parser.read_line("250-me").unwrap());
|
||||
assert!(parser.read_line("250-8BITMIME").unwrap());
|
||||
assert!(parser.read_line("250-SIZE 42").unwrap());
|
||||
assert!(!parser.read_line("250 AUTH PLAIN CRAM-MD5").unwrap());
|
||||
|
||||
let response = parser.response().unwrap();
|
||||
|
||||
assert_eq!(response,
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: 0,
|
||||
},
|
||||
message: vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_is_positive() {
|
||||
assert!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.is_positive());
|
||||
assert!(!Response::new(Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.is_positive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_message() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.message(),
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
|
||||
let empty_message: Vec<String> = vec![];
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![])
|
||||
.message(),
|
||||
empty_message);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_severity() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.severity(),
|
||||
Severity::PositiveCompletion);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "5".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.severity(),
|
||||
Severity::PermanentNegativeCompletion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_category() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.category(),
|
||||
Category::Unspecified4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_detail() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.detail(),
|
||||
1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_code() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.code(),
|
||||
"241");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_has_code() {
|
||||
assert!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.has_code(241));
|
||||
assert!(!Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.has_code(251));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_first_word() {
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.first_word(),
|
||||
Some("me".to_string()));
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string()])
|
||||
.first_word(),
|
||||
Some("me".to_string()));
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec![" ".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
assert_eq!(Response::new(Code {
|
||||
severity: "2".parse::<Severity>().unwrap(),
|
||||
category: "4".parse::<Category>().unwrap(),
|
||||
detail: 1,
|
||||
},
|
||||
vec!["".to_string()])
|
||||
.first_word(),
|
||||
None);
|
||||
}
|
||||
}
|
||||
298
src/sender.rs
298
src/sender.rs
@@ -1,298 +0,0 @@
|
||||
//! Sends an email using the client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
|
||||
use openssl::ssl::{SslMethod, SslContext};
|
||||
|
||||
use SMTP_PORT;
|
||||
use extension::{Extension, ServerInfo};
|
||||
use error::{SmtpResult, Error};
|
||||
use email::SendableEmail;
|
||||
use client::Client;
|
||||
use authentication::Mecanism;
|
||||
|
||||
/// Contains client configuration
|
||||
pub struct SenderBuilder {
|
||||
/// Maximum connection reuse
|
||||
///
|
||||
/// Zero means no limitation
|
||||
connection_reuse_count_limit: u16,
|
||||
/// Enable connection reuse
|
||||
enable_connection_reuse: bool,
|
||||
/// Name sent during HELO or EHLO
|
||||
hello_name: String,
|
||||
/// Credentials
|
||||
credentials: Option<(String, String)>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// SSL contexyt to use
|
||||
ssl_context: Option<SslContext>,
|
||||
/// List of authentication mecanism, sorted by priority
|
||||
authentication_mecanisms: Vec<Mecanism>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP Sender
|
||||
impl SenderBuilder {
|
||||
/// Creates a new local SMTP client
|
||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Result<SenderBuilder, Error> {
|
||||
let mut addresses = try!(addr.to_socket_addrs());
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SenderBuilder {
|
||||
server_addr: addr,
|
||||
ssl_context: None,
|
||||
credentials: None,
|
||||
connection_reuse_count_limit: 100,
|
||||
enable_connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
authentication_mecanisms: vec![Mecanism::CramMd5, Mecanism::Plain],
|
||||
}),
|
||||
None => Err(From::from("Could nor resolve hostname")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn localhost() -> Result<SenderBuilder, Error> {
|
||||
SenderBuilder::new(("localhost", SMTP_PORT))
|
||||
}
|
||||
|
||||
/// Use STARTTLS with a specific context
|
||||
pub fn ssl_context(mut self, ssl_context: SslContext) -> SenderBuilder {
|
||||
self.ssl_context = Some(ssl_context);
|
||||
self
|
||||
}
|
||||
|
||||
/// Require SSL/TLS using STARTTLS
|
||||
pub fn starttls(self) -> SenderBuilder {
|
||||
self.ssl_context(SslContext::new(SslMethod::Tlsv1).unwrap())
|
||||
}
|
||||
|
||||
/// Set the name used during HELO or EHLO
|
||||
pub fn hello_name(mut self, name: &str) -> SenderBuilder {
|
||||
self.hello_name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn enable_connection_reuse(mut self, enable: bool) -> SenderBuilder {
|
||||
self.enable_connection_reuse = enable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the maximum number of emails sent using one connection
|
||||
pub fn connection_reuse_count_limit(mut self, limit: u16) -> SenderBuilder {
|
||||
self.connection_reuse_count_limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials(mut self, username: &str, password: &str) -> SenderBuilder {
|
||||
self.credentials = Some((username.to_string(), password.to_string()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mecanisms
|
||||
pub fn authentication_mecanisms(mut self, mecanisms: Vec<Mecanism>) -> SenderBuilder {
|
||||
self.authentication_mecanisms = mecanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Sender`
|
||||
pub fn build(self) -> Sender {
|
||||
Sender::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a client
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
/// Panic state
|
||||
pub panic: bool,
|
||||
/// Connection reuse counter
|
||||
pub connection_reuse_count: u16,
|
||||
}
|
||||
|
||||
/// Structure that implements the high level SMTP client
|
||||
pub struct Sender {
|
||||
/// Information about the server
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<ServerInfo>,
|
||||
/// Sender variable states
|
||||
state: State,
|
||||
/// Information about the client
|
||||
client_info: SenderBuilder,
|
||||
/// Low level client
|
||||
client: Client,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.reset();
|
||||
}
|
||||
return Err(err)
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl Sender {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Sender`
|
||||
pub fn new(builder: SenderBuilder) -> Sender {
|
||||
|
||||
let client = Client::new();
|
||||
|
||||
Sender {
|
||||
client: client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset the client state
|
||||
fn reset(&mut self) {
|
||||
// Close the SMTP transaction if needed
|
||||
self.close();
|
||||
|
||||
// Reset the client state
|
||||
self.server_info = None;
|
||||
self.state.panic = false;
|
||||
self.state.connection_reuse_count = 0;
|
||||
}
|
||||
|
||||
/// Closes the inner connection
|
||||
pub fn close(&mut self) {
|
||||
self.client.close();
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
pub fn get_ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(self.client.ehlo(&self.client_info.hello_name), self);
|
||||
|
||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||
|
||||
// Print server information
|
||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||
|
||||
Ok(ehlo_response)
|
||||
}
|
||||
|
||||
/// Sends an email
|
||||
pub fn send<T: SendableEmail>(&mut self, email: T) -> SmtpResult {
|
||||
// Check if the connection is still available
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
if !self.client.is_connected() {
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// If there is a usable connection, test if the server answers and hello has been sent
|
||||
if self.state.connection_reuse_count == 0 {
|
||||
try!(self.client.connect(&self.client_info.server_addr));
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
try!(self.get_ehlo());
|
||||
|
||||
if self.client_info.ssl_context.is_some() {
|
||||
try_smtp!(self.client.starttls(), self);
|
||||
|
||||
try!(self.client
|
||||
.upgrade_tls_stream(self.client_info.ssl_context.as_ref().unwrap()));
|
||||
|
||||
try!(self.get_ehlo());
|
||||
}
|
||||
|
||||
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||
|
||||
let mut found = false;
|
||||
|
||||
for mecanism in self.client_info.authentication_mecanisms.clone() {
|
||||
if self.server_info.as_ref().unwrap().supports_auth_mecanism(mecanism) {
|
||||
found = true;
|
||||
let result = self.client.auth(mecanism, &username, &password);
|
||||
try_smtp!(result, self);
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
debug!("No supported authentication mecanisms available");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let current_message = try!(email.message_id().ok_or("Missing Message-ID"));
|
||||
let from_address = try!(email.from_address().ok_or("Missing From address"));
|
||||
let to_addresses = try!(email.to_addresses().ok_or("Missing To address"));
|
||||
let message = try!(email.message().ok_or("Missing message"));
|
||||
|
||||
// Mail
|
||||
let mail_options = match self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(&Extension::EightBitMime) {
|
||||
true => Some("BODY=8BITMIME"),
|
||||
false => None,
|
||||
};
|
||||
|
||||
try_smtp!(self.client.mail(&from_address, mail_options), self);
|
||||
|
||||
// Log the mail command
|
||||
info!("{}: from=<{}>", current_message, from_address);
|
||||
|
||||
// Recipient
|
||||
for to_address in to_addresses.iter() {
|
||||
try_smtp!(self.client.rcpt(&to_address), self);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", current_message, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.data(), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(&message);
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count = self.state.connection_reuse_count + 1;
|
||||
|
||||
// Log the message
|
||||
info!("{}: conn_use={}, size={}, status=sent ({})",
|
||||
current_message,
|
||||
self.state.connection_reuse_count,
|
||||
message.len(),
|
||||
result.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string()));
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
if (!self.client_info.enable_connection_reuse) ||
|
||||
(self.state.connection_reuse_count >= self.client_info.connection_reuse_count_limit) {
|
||||
self.reset();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
61
src/transport/file/error.rs
Normal file
61
src/transport/file/error.rs
Normal file
@@ -0,0 +1,61 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// JSON error
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(serde_json::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
Json(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
Error::Json(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
302
src/transport/file/mod.rs
Normal file
302
src/transport/file/mod.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.eml`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Transport, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Sync example with envelope
|
||||
//!
|
||||
//! It is possible to also write the envelope content in a separate JSON file
|
||||
//! by using the `with_envelope` builder. The JSON file will be written in the
|
||||
//! target directory with same name and a `json` extension.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "file-transport-envelope", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Transport, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::with_envelope(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "file-transport-envelope", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{Tokio1Transport, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "file-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use std::env::temp_dir;
|
||||
//! use lettre::{AsyncStd1Transport, Message, FileTransport};
|
||||
//!
|
||||
//! // Write to the local temp directory
|
||||
//! let sender = FileTransport::new(temp_dir());
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ---
|
||||
//!
|
||||
//! Example email content result
|
||||
//!
|
||||
//! ```eml
|
||||
//! From: NoBody <nobody@domain.tld>
|
||||
//! Reply-To: Yuin <yuin@domain.tld>
|
||||
//! To: Hei <hei@domain.tld>
|
||||
//! Subject: Happy new year
|
||||
//! Date: Tue, 18 Aug 2020 22:50:17 GMT
|
||||
//!
|
||||
//! Be happy!
|
||||
//! ```
|
||||
//!
|
||||
//! Example envelope result
|
||||
//!
|
||||
//! ```json
|
||||
//! {"forward_path":["hei@domain.tld"],"reverse_path":"nobody@domain.tld"}
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
use crate::address::Envelope;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Transport;
|
||||
use crate::Transport;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod error;
|
||||
|
||||
type Id = String;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: bool,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format.
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given directory
|
||||
///
|
||||
/// Writes the email content in eml format and the envelope
|
||||
/// in json format.
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
pub fn with_envelope<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
save_envelope: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn path(&self, email_id: &Uuid, extension: &str) -> PathBuf {
|
||||
self.path.join(format!("{}.{}", email_id, extension))
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use std::fs;
|
||||
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email)?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
fs::write(file, serde_json::to_string(&envelope)?)?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use async_std::fs;
|
||||
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email).await?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
fs::write(file, serde_json::to_string(&envelope)?).await?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::fs;
|
||||
|
||||
let email_id = Uuid::new_v4();
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email).await?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
fs::write(file, serde_json::to_string(&envelope)?).await?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl Tokio1Transport for FileTransport {
|
||||
type Ok = Id;
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio1_crate::fs;
|
||||
|
||||
let email_id = Uuid::new_v4();
|
||||
|
||||
let file = self.path(&email_id, "eml");
|
||||
fs::write(file, email).await?;
|
||||
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
{
|
||||
if self.save_envelope {
|
||||
let file = self.path(&email_id, "json");
|
||||
fs::write(file, serde_json::to_string(&envelope)?).await?;
|
||||
}
|
||||
}
|
||||
// use envelope anyway
|
||||
let _ = envelope;
|
||||
|
||||
Ok(email_id.to_string())
|
||||
}
|
||||
}
|
||||
28
src/transport/mod.rs
Normal file
28
src/transport/mod.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
//! ### Sending Messages
|
||||
//!
|
||||
//! This section explains how to manipulate emails you have created.
|
||||
//!
|
||||
//! This mailer contains several different transports for your emails. To be sendable, the
|
||||
//! emails have to implement `Email`, which is the case for emails created with `lettre::builder`.
|
||||
//!
|
||||
//! The following transports are available:
|
||||
//!
|
||||
//! * The `SmtpTransport` uses the SMTP protocol to send the message over the network. It is
|
||||
//! the preferred way of sending emails.
|
||||
//! * The `SendmailTransport` uses the sendmail command to send messages. It is an alternative to
|
||||
//! the SMTP transport.
|
||||
//! * The `FileTransport` creates a file containing the email content to be sent. It can be used
|
||||
//! for debugging or if you want to keep all sent emails.
|
||||
//! * The `StubTransport` is useful for debugging, and only prints the content of the email in the
|
||||
//! logs.
|
||||
|
||||
#[cfg(feature = "file-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "file-transport")))]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "sendmail-transport")))]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "smtp-transport")))]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
52
src/transport/sendmail/error.rs
Normal file
52
src/transport/sendmail/error.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use self::Error::*;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
string::FromUtf8Error,
|
||||
};
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(String),
|
||||
/// Error parsing UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match *self {
|
||||
Client(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
}
|
||||
309
src/transport/sendmail/mod.rs
Normal file
309
src/transport/sendmail/mod.rs
Normal file
@@ -0,0 +1,309 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
//! ## Sync example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! # use lettre::{Message, Transport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//!
|
||||
//! # #[cfg(not(all(feature = "sendmail-transport", feature = "builder")))]
|
||||
//! # fn main() {}
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 0.2 example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "tokio02", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, Tokio02Transport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async tokio 1.x example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "tokio1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, Tokio1Transport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Async async-std 1.x example
|
||||
//!
|
||||
//!```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(all(feature = "async-std1", feature = "sendmail-transport", feature = "builder"))]
|
||||
//! # async fn run() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::{Message, AsyncStd1Transport, SendmailTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let sender = SendmailTransport::new();
|
||||
//! let result = sender.send(email).await;
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub use self::error::Error;
|
||||
use crate::address::Envelope;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Transport;
|
||||
use crate::Transport;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02", feature = "tokio1"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
io::prelude::*,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
mod error;
|
||||
|
||||
const DEFAUT_SENDMAIL: &str = "/usr/sbin/sendmail";
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct SendmailTransport {
|
||||
command: OsString,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: DEFAUT_SENDMAIL.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<OsString>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn command(&self, envelope: &Envelope) -> Command {
|
||||
let mut c = Command::new(&self.command);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
fn tokio02_command(&self, envelope: &Envelope) -> tokio02_crate::process::Command {
|
||||
use tokio02_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
fn tokio1_command(&self, envelope: &Envelope) -> tokio1_crate::process::Command {
|
||||
use tokio1_crate::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.command);
|
||||
c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
fn async_std_command(&self, envelope: &Envelope) -> async_std::process::Command {
|
||||
use async_std::process::Command;
|
||||
|
||||
let mut c = Command::new(&self.command);
|
||||
// TODO: figure out why enabling this kills it earlier
|
||||
// c.kill_on_drop(true);
|
||||
c.arg("-i");
|
||||
if let Some(from) = envelope.from() {
|
||||
c.arg("-f").arg(from);
|
||||
}
|
||||
c.arg("--")
|
||||
.args(envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
c
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SendmailTransport {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
// Spawn the sendmail command
|
||||
let mut process = self.command(envelope).spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(email)?;
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(error::Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use async_std::io::prelude::WriteExt;
|
||||
|
||||
let mut command = self.async_std_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio02_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio02_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl Tokio1Transport for SendmailTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
use tokio1_crate::io::AsyncWriteExt;
|
||||
|
||||
let mut command = self.tokio1_command(envelope);
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = command.spawn()?;
|
||||
|
||||
process.stdin.as_mut().unwrap().write_all(&email).await?;
|
||||
let output = process.wait_with_output().await?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client(String::from_utf8(output.stderr)?))
|
||||
}
|
||||
}
|
||||
}
|
||||
401
src/transport/smtp/async_transport.rs
Normal file
401
src/transport/smtp/async_transport.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
use super::Tls;
|
||||
use super::{
|
||||
client::AsyncSmtpConnection, ClientId, Credentials, Error, Mechanism, Response, SmtpInfo,
|
||||
};
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
use crate::Envelope;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use crate::Tokio1Transport;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpTransport<C> {
|
||||
// TODO: pool
|
||||
inner: AsyncSmtpClient<C>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for AsyncSmtpTransport<Tokio02Connector> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[async_trait]
|
||||
impl Tokio1Transport for AsyncSmtpTransport<Tokio1Connector> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for AsyncSmtpTransport<AsyncStd1Connector> {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
let mut conn = self.inner.connection().await?;
|
||||
|
||||
let result = conn.send(envelope, email).await?;
|
||||
|
||||
conn.quit().await?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsyncSmtpTransport<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
pub fn relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSIONS_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`AsyncSmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
pub fn starttls_relay(relay: &str) -> Result<AsyncSmtpTransportBuilder, Error> {
|
||||
use super::{TlsParameters, SUBMISSION_PORT};
|
||||
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> AsyncSmtpTransport<C> {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`AsyncSmtpTransport::relay`](#method.relay) or
|
||||
/// [`AsyncSmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> AsyncSmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
AsyncSmtpTransportBuilder { info: new }
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`AsyncSmtpTransport`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `AsyncSmtpTransport`
|
||||
impl AsyncSmtpTransportBuilder {
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport (with default pool if enabled)
|
||||
pub fn build<C>(self) -> AsyncSmtpTransport<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
let connector = Default::default();
|
||||
let client = AsyncSmtpClient {
|
||||
connector,
|
||||
info: self.info,
|
||||
};
|
||||
AsyncSmtpTransport { inner: client }
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
pub struct AsyncSmtpClient<C> {
|
||||
connector: C,
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
impl<C> AsyncSmtpClient<C>
|
||||
where
|
||||
C: AsyncSmtpConnector,
|
||||
{
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub async fn connection(&self) -> Result<AsyncSmtpConnection, Error> {
|
||||
let mut conn = C::connect(
|
||||
&self.info.server,
|
||||
self.info.port,
|
||||
&self.info.hello_name,
|
||||
&self.info.tls,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials).await?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait AsyncSmtpConnector: Default + private::Sealed {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio02")))]
|
||||
pub struct Tokio02Connector;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl AsyncSmtpConnector for Tokio02Connector {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio02(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[cfg(feature = "tokio1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "tokio1")))]
|
||||
pub struct Tokio1Connector;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl AsyncSmtpConnector for Tokio1Connector {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
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"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_tokio1(hostname, port, hello_name, tls_parameters).await?;
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "async-std1")))]
|
||||
pub struct AsyncStd1Connector;
|
||||
|
||||
#[async_trait]
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl AsyncSmtpConnector for AsyncStd1Connector {
|
||||
async fn connect(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls: &Tls,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match tls {
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters.clone()),
|
||||
_ => None,
|
||||
};
|
||||
#[allow(unused_mut)]
|
||||
let mut conn =
|
||||
AsyncSmtpConnection::connect_asyncstd1(hostname, port, hello_name, tls_parameters)
|
||||
.await?;
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
match tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters.clone(), hello_name).await?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
use super::*;
|
||||
|
||||
pub trait Sealed {}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
impl Sealed for Tokio02Connector {}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
impl Sealed for Tokio1Connector {}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
impl Sealed for AsyncStd1Connector {}
|
||||
}
|
||||
170
src/transport/smtp/authentication.rs
Normal file
170
src/transport/smtp/authentication.rs
Normal file
@@ -0,0 +1,170 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use crate::transport::smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Create a `Credentials` struct from username and password
|
||||
pub fn new(username: String, password: String) -> Credentials {
|
||||
Credentials {
|
||||
authentication_identity: username,
|
||||
secret: password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> From<(S, T)> for Credentials
|
||||
where
|
||||
S: Into<String>,
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from((username, password): (S, T)) -> Self {
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// 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 {
|
||||
f.write_str(match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | 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());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_user_pass_for_credentials() {
|
||||
assert_eq!(
|
||||
Credentials::new("alice".to_string(), "wonderland".to_string()),
|
||||
Credentials::from(("alice", "wonderland"))
|
||||
);
|
||||
}
|
||||
}
|
||||
296
src/transport/smtp/client/async_connection.rs
Normal file
296
src/transport/smtp/client/async_connection.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use std::{fmt::Display, io};
|
||||
|
||||
use futures_util::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
|
||||
use super::{AsyncNetworkStream, ClientCodec, TlsParameters};
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
},
|
||||
Envelope,
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort().await;
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct AsyncSmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<AsyncNetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl AsyncSmtpConnection {
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub async fn connect_tokio02(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio02(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_tokio1(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = AsyncNetworkStream::connect_asyncstd1(hostname, port, tls_parameters).await?;
|
||||
Self::connect_impl(stream, hello_name).await
|
||||
}
|
||||
|
||||
async fn connect_impl(
|
||||
stream: AsyncNetworkStream,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<AsyncSmtpConnection, Error> {
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = AsyncSmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
// TODO log
|
||||
let _response = conn.read_response().await?;
|
||||
|
||||
conn.ehlo(hello_name).await?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub async fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options))
|
||||
.await,
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(
|
||||
self.command(Rcpt::new(to_address.clone(), vec![])).await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data).await, self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email).await, self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub async fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
try_smtp!(self.command(Starttls).await, self);
|
||||
try_smtp!(
|
||||
self.stream.get_mut().upgrade_tls(tls_parameters).await,
|
||||
self
|
||||
);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name).await, self);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
async fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())).await, self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit).await, self))
|
||||
}
|
||||
|
||||
pub async fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: AsyncNetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub async fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).await.is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub async fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self
|
||||
.command(Auth::new(mechanism, credentials.clone(), None)?)
|
||||
.await?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)
|
||||
.await,
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub async fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice()).await?;
|
||||
self.write(b"\r\n.\r\n").await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub async fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes()).await?;
|
||||
self.read_response().await
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
async fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string).await?;
|
||||
self.stream.get_mut().flush().await?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub async fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer).await? > 0 {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
}
|
||||
}
|
||||
558
src/transport/smtp/client/async_net.rs
Normal file
558
src/transport/smtp/client/async_net.rs
Normal file
@@ -0,0 +1,558 @@
|
||||
#[cfg(any(
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_io::{AsyncRead as FuturesAsyncRead, AsyncWrite as FuturesAsyncWrite};
|
||||
use futures_io::{Error as IoError, ErrorKind, Result as IoResult};
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::io::{AsyncRead as _, AsyncWrite as _};
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::io::{AsyncRead as _, AsyncWrite as _, ReadBuf as Tokio1ReadBuf};
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
use async_std::net::TcpStream as AsyncStd1TcpStream;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate::net::TcpStream as Tokio02TcpStream;
|
||||
#[cfg(feature = "tokio1")]
|
||||
use tokio1_crate::net::TcpStream as Tokio1TcpStream;
|
||||
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
use async_native_tls::TlsStream as AsyncStd1TlsStream;
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
use tokio02_native_tls_crate::TlsStream as Tokio02TlsStream;
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
use tokio1_native_tls_crate::TlsStream as Tokio1TlsStream;
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
use async_rustls::client::TlsStream as AsyncStd1RustlsTlsStream;
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
use tokio02_rustls::client::TlsStream as Tokio02RustlsTlsStream;
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
use tokio1_rustls::client::TlsStream as Tokio1RustlsTlsStream;
|
||||
|
||||
#[cfg(any(
|
||||
feature = "tokio02-native-tls",
|
||||
feature = "tokio02-rustls-tls",
|
||||
feature = "tokio1-native-tls",
|
||||
feature = "tokio1-rustls-tls",
|
||||
feature = "async-std1-native-tls",
|
||||
feature = "async-std1-rustls-tls"
|
||||
))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::TlsParameters;
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// A network stream
|
||||
pub struct AsyncNetworkStream {
|
||||
inner: InnerAsyncNetworkStream,
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[allow(dead_code)]
|
||||
enum InnerAsyncNetworkStream {
|
||||
/// Plain Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02")]
|
||||
Tokio02Tcp(Tokio02TcpStream),
|
||||
/// Encrypted Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
Tokio02NativeTls(Tokio02TlsStream<Tokio02TcpStream>),
|
||||
/// Encrypted Tokio 0.2 TCP stream
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
Tokio02RustlsTls(Tokio02RustlsTlsStream<Tokio02TcpStream>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1")]
|
||||
Tokio1Tcp(Tokio1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
Tokio1NativeTls(Tokio1TlsStream<Tokio1TcpStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
Tokio1RustlsTls(Tokio1RustlsTlsStream<Tokio1TcpStream>),
|
||||
/// Plain Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1")]
|
||||
AsyncStd1Tcp(AsyncStd1TcpStream),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
AsyncStd1NativeTls(AsyncStd1TlsStream<AsyncStd1TcpStream>),
|
||||
/// Encrypted Tokio 1.x TCP stream
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
AsyncStd1RustlsTls(AsyncStd1RustlsTlsStream<AsyncStd1TcpStream>),
|
||||
/// Can't be built
|
||||
None,
|
||||
}
|
||||
|
||||
impl AsyncNetworkStream {
|
||||
fn new(inner: InnerAsyncNetworkStream) -> Self {
|
||||
if let InnerAsyncNetworkStream::None = inner {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
}
|
||||
|
||||
AsyncNetworkStream { inner }
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> IoResult<SocketAddr> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref s) => {
|
||||
s.get_ref().get_ref().get_ref().peer_addr()
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref s) => s.get_ref().0.peer_addr(),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"InnerAsyncNetworkStream::None must never be built",
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub async fn connect_tokio02(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = Tokio02TcpStream::connect((hostname, port)).await?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub async fn connect_tokio1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = Tokio1TcpStream::connect((hostname, port)).await?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub async fn connect_asyncstd1(
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
tls_parameters: Option<TlsParameters>,
|
||||
) -> Result<AsyncNetworkStream, Error> {
|
||||
let tcp_stream = AsyncStd1TcpStream::connect((hostname, port)).await?;
|
||||
|
||||
let mut stream = AsyncNetworkStream::new(InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters).await?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub async fn upgrade_tls(&mut self, tls_parameters: TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(all(
|
||||
feature = "tokio02",
|
||||
not(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio02-native-tls or the tokio02-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio02_tls(tcp_stream, tls_parameters).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
feature = "tokio1",
|
||||
not(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the tokio1-native-tls or the tokio1-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tokio1_tls(tcp_stream, tls_parameters).await?;
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(all(
|
||||
feature = "async-std1",
|
||||
not(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))
|
||||
))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an AsyncNetworkStream without having enabled either the async-std1-native-tls or the async-std1-rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream = std::mem::replace(&mut self.inner, InnerAsyncNetworkStream::None);
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_asyncstd1_tls(tcp_stream, tls_parameters).await?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "tokio02-native-tls", feature = "tokio02-rustls-tls"))]
|
||||
async fn upgrade_tokio02_tls(
|
||||
tcp_stream: Tokio02TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
#[cfg(not(feature = "tokio02-native-tls"))]
|
||||
panic!("built without the tokio02-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
return {
|
||||
use tokio02_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio02-rustls-tls"))]
|
||||
panic!("built without the tokio02-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
return {
|
||||
use tokio02_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio02RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "tokio1-native-tls", feature = "tokio1-rustls-tls"))]
|
||||
async fn upgrade_tokio1_tls(
|
||||
tcp_stream: Tokio1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
#[cfg(not(feature = "tokio1-native-tls"))]
|
||||
panic!("built without the tokio1-native-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
return {
|
||||
use tokio1_native_tls_crate::TlsConnector;
|
||||
|
||||
let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1NativeTls(stream))
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "tokio1-rustls-tls"))]
|
||||
panic!("built without the tokio1-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
return {
|
||||
use tokio1_rustls::{webpki::DNSNameRef, TlsConnector};
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::Tokio1RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[cfg(any(feature = "async-std1-native-tls", feature = "async-std1-rustls-tls"))]
|
||||
async fn upgrade_asyncstd1_tls(
|
||||
tcp_stream: AsyncStd1TcpStream,
|
||||
mut tls_parameters: TlsParameters,
|
||||
) -> Result<InnerAsyncNetworkStream, Error> {
|
||||
let domain = std::mem::take(&mut tls_parameters.domain);
|
||||
|
||||
match tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
panic!("native-tls isn't supported with async-std yet. See https://github.com/lettre/lettre/pull/531#issuecomment-757893531");
|
||||
|
||||
/*
|
||||
#[cfg(not(feature = "async-std1-native-tls"))]
|
||||
panic!("built without the async-std1-native-tls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
return {
|
||||
use async_native_tls::TlsConnector;
|
||||
|
||||
// TODO: fix
|
||||
let connector: TlsConnector = todo!();
|
||||
// let connector = TlsConnector::from(connector);
|
||||
let stream = connector.connect(&domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1NativeTls(stream))
|
||||
};
|
||||
*/
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(config) => {
|
||||
#[cfg(not(feature = "async-std1-rustls-tls"))]
|
||||
panic!("built without the async-std1-rustls-tls feature");
|
||||
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
return {
|
||||
use async_rustls::webpki::DNSNameRef;
|
||||
use async_rustls::TlsConnector;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(&domain)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = connector.connect(domain, tcp_stream).await?;
|
||||
Ok(InnerAsyncNetworkStream::AsyncStd1RustlsTls(stream))
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(_) => false,
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(_) => true,
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(_) => false,
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(_) => true,
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(_) => true,
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(_) => false,
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(_) => true,
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(_) => true,
|
||||
InnerAsyncNetworkStream::None => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FuturesAsyncRead for AsyncNetworkStream {
|
||||
fn poll_read(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => {
|
||||
let mut b = Tokio1ReadBuf::new(buf);
|
||||
match Pin::new(s).poll_read(cx, &mut b) {
|
||||
Poll::Ready(Ok(())) => Poll::Ready(Ok(b.filled().len())),
|
||||
Poll::Ready(Err(err)) => Poll::Ready(Err(err)),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_read(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_read(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FuturesAsyncWrite for AsyncNetworkStream {
|
||||
fn poll_write(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> Poll<IoResult<usize>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_write(cx, buf),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => {
|
||||
Pin::new(s).poll_write(cx, buf)
|
||||
}
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_flush(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<IoResult<()>> {
|
||||
match self.inner {
|
||||
#[cfg(feature = "tokio02")]
|
||||
InnerAsyncNetworkStream::Tokio02Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio02-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio02-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio02RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1")]
|
||||
InnerAsyncNetworkStream::Tokio1Tcp(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-native-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1NativeTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "tokio1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::Tokio1RustlsTls(ref mut s) => Pin::new(s).poll_shutdown(cx),
|
||||
#[cfg(feature = "async-std1")]
|
||||
InnerAsyncNetworkStream::AsyncStd1Tcp(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-native-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1NativeTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
#[cfg(feature = "async-std1-rustls-tls")]
|
||||
InnerAsyncNetworkStream::AsyncStd1RustlsTls(ref mut s) => Pin::new(s).poll_close(cx),
|
||||
InnerAsyncNetworkStream::None => {
|
||||
debug_assert!(false, "InnerAsyncNetworkStream::None must never be built");
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
267
src/transport/smtp/client/connection.rs
Normal file
267
src/transport/smtp/client/connection.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io::{self, BufRead, BufReader, Write},
|
||||
net::ToSocketAddrs,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use super::{ClientCodec, NetworkStream, TlsParameters};
|
||||
use crate::address::Envelope;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
commands::*,
|
||||
error::Error,
|
||||
extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
|
||||
response::{parse_response, Response},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
use super::escape_crlf;
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
$client.abort();
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct SmtpConnection {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: BufReader<NetworkStream>,
|
||||
/// Panic state
|
||||
panic: bool,
|
||||
/// Information about the server
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
impl SmtpConnection {
|
||||
pub fn server_info(&self) -> &ServerInfo {
|
||||
&self.server_info
|
||||
}
|
||||
|
||||
// FIXME add simple connect and rename this one
|
||||
|
||||
/// Connects to the configured server
|
||||
///
|
||||
/// Sends EHLO and parses server information
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
server: A,
|
||||
timeout: Option<Duration>,
|
||||
hello_name: &ClientId,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
) -> Result<SmtpConnection, Error> {
|
||||
let stream = NetworkStream::connect(server, timeout, tls_parameters)?;
|
||||
let stream = BufReader::new(stream);
|
||||
let mut conn = SmtpConnection {
|
||||
stream,
|
||||
panic: false,
|
||||
server_info: ServerInfo::default(),
|
||||
};
|
||||
conn.set_timeout(timeout)?;
|
||||
// TODO log
|
||||
let _response = conn.read_response()?;
|
||||
|
||||
conn.ehlo(hello_name)?;
|
||||
|
||||
// Print server information
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("server {}", conn.server_info);
|
||||
Ok(conn)
|
||||
}
|
||||
|
||||
pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self.server_info().supports_feature(Extension::EightBitMime) {
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
try_smtp!(
|
||||
self.command(Mail::new(envelope.from().cloned(), mail_options)),
|
||||
self
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in envelope.to() {
|
||||
try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.command(Data), self);
|
||||
|
||||
// Message content
|
||||
let result = try_smtp!(self.message(email), self);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn has_broken(&self) -> bool {
|
||||
self.panic
|
||||
}
|
||||
|
||||
pub fn can_starttls(&self) -> bool {
|
||||
!self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub fn starttls(
|
||||
&mut self,
|
||||
tls_parameters: &TlsParameters,
|
||||
hello_name: &ClientId,
|
||||
) -> Result<(), Error> {
|
||||
if self.server_info.supports_feature(Extension::StartTls) {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
{
|
||||
try_smtp!(self.command(Starttls), self);
|
||||
try_smtp!(self.stream.get_mut().upgrade_tls(tls_parameters), self);
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("connection encrypted");
|
||||
// Send EHLO again
|
||||
try_smtp!(self.ehlo(hello_name), self);
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
// This should never happen as `Tls` can only be created
|
||||
// when a TLS library is enabled
|
||||
unreachable!("TLS support required but not supported");
|
||||
} else {
|
||||
Err(Error::Client("STARTTLS is not supported on this server"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send EHLO and update server info
|
||||
fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
|
||||
let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
|
||||
self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) -> Result<Response, Error> {
|
||||
Ok(try_smtp!(self.command(Quit), self))
|
||||
}
|
||||
|
||||
pub fn abort(&mut self) {
|
||||
// Only try to quit if we are not already broken
|
||||
if !self.panic {
|
||||
self.panic = true;
|
||||
let _ = self.command(Quit);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: NetworkStream) {
|
||||
self.stream = BufReader::new(stream);
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.stream.get_ref().is_encrypted()
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
self.stream.get_mut().set_read_timeout(duration)?;
|
||||
self.stream.get_mut().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
pub fn test_connected(&mut self) -> bool {
|
||||
self.command(Noop).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(
|
||||
&mut self,
|
||||
mechanisms: &[Mechanism],
|
||||
credentials: &Credentials,
|
||||
) -> Result<Response, Error> {
|
||||
let mechanism = self
|
||||
.server_info
|
||||
.get_auth_mechanism(mechanisms)
|
||||
.ok_or(Error::Client(
|
||||
"No compatible authentication mechanism was found",
|
||||
))?;
|
||||
|
||||
// Limit challenges to avoid blocking
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = try_smtp!(
|
||||
self.command(Auth::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?),
|
||||
self
|
||||
);
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
codec.encode(message, &mut out_buf);
|
||||
self.write(out_buf.as_slice())?;
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
self.stream.get_mut().write_all(string)?;
|
||||
self.stream.get_mut().flush()?;
|
||||
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
pub fn read_response(&mut self) -> Result<Response, Error> {
|
||||
let mut buffer = String::with_capacity(100);
|
||||
|
||||
while self.stream.read_line(&mut buffer)? > 0 {
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("<< {}", escape_crlf(&buffer));
|
||||
match parse_response(&buffer) {
|
||||
Ok((_remaining, response)) => {
|
||||
if response.is_positive() {
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
return Err(response.into());
|
||||
}
|
||||
Err(nom::Err::Failure(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
}
|
||||
Err(nom::Err::Incomplete(_)) => { /* read more */ }
|
||||
Err(nom::Err::Error(e)) => {
|
||||
return Err(Error::Parsing(e.code));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(io::Error::new(io::ErrorKind::Other, "incomplete").into())
|
||||
}
|
||||
}
|
||||
122
src/transport/smtp/client/mock.rs
Normal file
122
src/transport/smtp/client/mock.rs
Normal file
@@ -0,0 +1,122 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::{
|
||||
io::{self, Cursor, Read, Write},
|
||||
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_all(&[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_all(&[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_all(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
}
|
||||
}
|
||||
140
src/transport/smtp/client/mod.rs
Normal file
140
src/transport/smtp/client/mod.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! SMTP client
|
||||
//!
|
||||
//! `SmtpConnection` allows manually sending SMTP commands.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # use std::error::Error;
|
||||
//!
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! use lettre::transport::smtp::{SMTP_PORT, extension::ClientId, commands::*, client::SmtpConnection};
|
||||
//!
|
||||
//! let hello = ClientId::Domain("my_hostname".to_string());
|
||||
//! let mut client = SmtpConnection::connect(&("localhost", SMTP_PORT), None, &hello, None)?;
|
||||
//! client.command(
|
||||
//! Mail::new(Some("user@example.com".parse()?), vec![])
|
||||
//! )?;
|
||||
//! client.command(
|
||||
//! Rcpt::new("user@example.org".parse()?, vec![])
|
||||
//! )?;
|
||||
//! client.command(Data)?;
|
||||
//! client.message("Test email".as_bytes())?;
|
||||
//! client.command(Quit)?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_connection::AsyncSmtpConnection;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub(crate) use self::async_net::AsyncNetworkStream;
|
||||
use self::net::NetworkStream;
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub(super) use self::tls::InnerTlsParameters;
|
||||
pub use self::{
|
||||
connection::SmtpConnection,
|
||||
mock::MockStream,
|
||||
tls::{Certificate, Tls, TlsParameters, TlsParametersBuilder},
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_connection;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_net;
|
||||
mod connection;
|
||||
mod mock;
|
||||
mod net;
|
||||
mod tls;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ClientCodec {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
}
|
||||
|
||||
/// Adds transparency
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.extend_from_slice(b"\r\n.\r\n"),
|
||||
1 => buf.extend_from_slice(b"\n.\r\n"),
|
||||
2 => buf.extend_from_slice(b".\r\n"),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.escape_count = 0;
|
||||
}
|
||||
_ => {
|
||||
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.extend_from_slice(&frame[start..idx]);
|
||||
buf.extend_from_slice(b".");
|
||||
start = idx;
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(&frame[start..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
#[cfg(feature = "tracing")]
|
||||
pub(super) fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
|
||||
codec.encode(b"test\r\n", &mut buf);
|
||||
codec.encode(b".\r\n", &mut buf);
|
||||
codec.encode(b"\r\ntest", &mut buf);
|
||||
codec.encode(b"te\r\n.\r\nst", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
codec.encode(b"test.", &mut buf);
|
||||
codec.encode(b"test\n", &mut buf);
|
||||
codec.encode(b".test\n", &mut buf);
|
||||
codec.encode(b"test", &mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "log")]
|
||||
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>"
|
||||
);
|
||||
}
|
||||
}
|
||||
240
src/transport/smtp/client/net.rs
Normal file
240
src/transport/smtp/client/net.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
io::{self, Read, Write},
|
||||
net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::TlsStream;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientSession, StreamOwned};
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::InnerTlsParameters;
|
||||
use super::{MockStream, TlsParameters};
|
||||
use crate::transport::smtp::Error;
|
||||
|
||||
/// A network stream
|
||||
pub struct NetworkStream {
|
||||
inner: InnerNetworkStream,
|
||||
}
|
||||
|
||||
/// Represents the different types of underlying network streams
|
||||
// usually only one TLS backend at a time is going to be enabled,
|
||||
// so clippy::large_enum_variant doesn't make sense here
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum InnerNetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsStream<TcpStream>),
|
||||
/// Encrypted TCP stream
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(StreamOwned<ClientSession, TcpStream>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
fn new(inner: InnerNetworkStream) -> Self {
|
||||
NetworkStream { inner }
|
||||
}
|
||||
|
||||
pub fn new_mock(mock: MockStream) -> Self {
|
||||
Self::new(InnerNetworkStream::Mock(mock))
|
||||
}
|
||||
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().peer_addr(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().peer_addr(),
|
||||
InnerNetworkStream::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.inner {
|
||||
InnerNetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref s) => s.get_ref().shutdown(how),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref s) => s.get_ref().shutdown(how),
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Option<Duration>,
|
||||
tls_parameters: Option<&TlsParameters>,
|
||||
) -> Result<NetworkStream, Error> {
|
||||
fn try_connect_timeout<T: ToSocketAddrs>(
|
||||
server: T,
|
||||
timeout: Duration,
|
||||
) -> Result<TcpStream, Error> {
|
||||
let addrs = server.to_socket_addrs()?;
|
||||
for addr in addrs {
|
||||
if let Ok(result) = TcpStream::connect_timeout(&addr, timeout) {
|
||||
return Ok(result);
|
||||
}
|
||||
}
|
||||
Err(Error::Client("Could not connect"))
|
||||
}
|
||||
|
||||
let tcp_stream = match timeout {
|
||||
Some(t) => try_connect_timeout(server, t)?,
|
||||
None => TcpStream::connect(server)?,
|
||||
};
|
||||
|
||||
let mut stream = NetworkStream::new(InnerNetworkStream::Tcp(tcp_stream));
|
||||
if let Some(tls_parameters) = tls_parameters {
|
||||
stream.upgrade_tls(tls_parameters)?;
|
||||
}
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn upgrade_tls(&mut self, tls_parameters: &TlsParameters) -> Result<(), Error> {
|
||||
match &self.inner {
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
let _ = tls_parameters;
|
||||
panic!("Trying to upgrade an NetworkStream without having enabled either the native-tls or the rustls-tls feature");
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
InnerNetworkStream::Tcp(_) => {
|
||||
// get owned TcpStream
|
||||
let tcp_stream =
|
||||
std::mem::replace(&mut self.inner, InnerNetworkStream::Mock(MockStream::new()));
|
||||
let tcp_stream = match tcp_stream {
|
||||
InnerNetworkStream::Tcp(tcp_stream) => tcp_stream,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
self.inner = Self::upgrade_tls_impl(tcp_stream, tls_parameters)?;
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
fn upgrade_tls_impl(
|
||||
tcp_stream: TcpStream,
|
||||
tls_parameters: &TlsParameters,
|
||||
) -> Result<InnerNetworkStream, Error> {
|
||||
Ok(match &tls_parameters.connector {
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerTlsParameters::NativeTls(connector) => {
|
||||
let stream = connector
|
||||
.connect(tls_parameters.domain(), tcp_stream)
|
||||
.map_err(|err| Error::Io(io::Error::new(io::ErrorKind::Other, err)))?;
|
||||
InnerNetworkStream::NativeTls(stream)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerTlsParameters::RustlsTls(connector) => {
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
let domain = DNSNameRef::try_from_ascii_str(tls_parameters.domain())?;
|
||||
let stream = StreamOwned::new(
|
||||
ClientSession::new(&Arc::new(connector.clone()), domain),
|
||||
tcp_stream,
|
||||
);
|
||||
|
||||
InnerNetworkStream::RustlsTls(stream)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(_) | InnerNetworkStream::Mock(_) => false,
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(_) => true,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(_) => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_read_timeout(duration)
|
||||
}
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
pub fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut stream) => {
|
||||
stream.get_ref().set_write_timeout(duration)
|
||||
}
|
||||
|
||||
InnerNetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.read(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.read(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.write(buf),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.write(buf),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match self.inner {
|
||||
InnerNetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
#[cfg(feature = "native-tls")]
|
||||
InnerNetworkStream::NativeTls(ref mut s) => s.flush(),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InnerNetworkStream::RustlsTls(ref mut s) => s.flush(),
|
||||
InnerNetworkStream::Mock(ref mut s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
286
src/transport/smtp/client/tls.rs
Normal file
286
src/transport/smtp/client/tls.rs
Normal file
@@ -0,0 +1,286 @@
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use rustls::{ClientConfig, RootCertStore, ServerCertVerified, ServerCertVerifier, TLSError};
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
use webpki::DNSNameRef;
|
||||
|
||||
use crate::transport::smtp::error::Error;
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
// This is also rustls' default behavior
|
||||
#[cfg(feature = "native-tls")]
|
||||
const DEFAULT_TLS_MIN_PROTOCOL: Protocol = Protocol::Tlsv12;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub enum Tls {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Opportunistic(TlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Required(TlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Wrapper(TlsParameters),
|
||||
}
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct TlsParameters {
|
||||
pub(crate) connector: InnerTlsParameters,
|
||||
/// The domain name which is expected in the TLS certificate from the server
|
||||
pub(super) domain: String,
|
||||
}
|
||||
|
||||
/// Builder for `TlsParameters`
|
||||
#[derive(Clone)]
|
||||
pub struct TlsParametersBuilder {
|
||||
domain: String,
|
||||
root_certs: Vec<Certificate>,
|
||||
accept_invalid_hostnames: bool,
|
||||
accept_invalid_certs: bool,
|
||||
}
|
||||
|
||||
impl TlsParametersBuilder {
|
||||
/// Creates a new builder for `TlsParameters`
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self {
|
||||
domain,
|
||||
root_certs: Vec::new(),
|
||||
accept_invalid_hostnames: false,
|
||||
accept_invalid_certs: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a custom root certificate
|
||||
///
|
||||
/// Can be used to safely connect to a server using a self signed certificate, for example.
|
||||
pub fn add_root_certificate(&mut self, cert: Certificate) -> &mut Self {
|
||||
self.root_certs.push(cert);
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether certificates with an invalid hostname are accepted
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If hostname verification is disabled *any* valid certificate,
|
||||
/// including those from other sites, are trusted.
|
||||
///
|
||||
/// This method introduces significant vulnerabilities to man-in-the-middle attacks.
|
||||
///
|
||||
/// Hostname verification can only be disabled with the `native-tls` TLS backend.
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn dangerous_accept_invalid_hostnames(
|
||||
&mut self,
|
||||
accept_invalid_hostnames: bool,
|
||||
) -> &mut Self {
|
||||
self.accept_invalid_hostnames = accept_invalid_hostnames;
|
||||
self
|
||||
}
|
||||
|
||||
/// Controls whether invalid certificates are accepted
|
||||
///
|
||||
/// Defaults to `false`.
|
||||
///
|
||||
/// # Warning
|
||||
///
|
||||
/// You should think very carefully before using this method.
|
||||
/// If certificate verification is disabled, *any* certificate
|
||||
/// is trusted for use, including:
|
||||
///
|
||||
/// * Self signed certificates
|
||||
/// * Certificates from different hostnames
|
||||
/// * Expired certificates
|
||||
///
|
||||
/// This method should only be used as a last resort, as it introduces
|
||||
/// significant vulnerabilities to man-in-the-middle attacks.
|
||||
pub fn dangerous_accept_invalid_certs(&mut self, accept_invalid_certs: bool) -> &mut Self {
|
||||
self.accept_invalid_certs = accept_invalid_certs;
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
// TODO: remove below line once native-tls is supported with async-std
|
||||
#[allow(unreachable_code)]
|
||||
pub fn build(self) -> Result<TlsParameters, Error> {
|
||||
// TODO: remove once native-tls is supported with async-std
|
||||
#[cfg(all(feature = "rustls-tls", feature = "async-std1"))]
|
||||
return self.build_rustls();
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
return self.build_native();
|
||||
|
||||
#[cfg(not(feature = "native-tls"))]
|
||||
return self.build_rustls();
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls with the provided configuration
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn build_native(self) -> Result<TlsParameters, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
|
||||
for cert in self.root_certs {
|
||||
tls_builder.add_root_certificate(cert.native_tls);
|
||||
}
|
||||
tls_builder.danger_accept_invalid_hostnames(self.accept_invalid_hostnames);
|
||||
tls_builder.danger_accept_invalid_certs(self.accept_invalid_certs);
|
||||
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_MIN_PROTOCOL));
|
||||
let connector = tls_builder.build()?;
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::NativeTls(connector),
|
||||
domain: self.domain,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls with the provided configuration
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn build_rustls(self) -> Result<TlsParameters, Error> {
|
||||
use webpki_roots::TLS_SERVER_ROOTS;
|
||||
|
||||
let mut tls = ClientConfig::new();
|
||||
|
||||
for cert in self.root_certs {
|
||||
for rustls_cert in cert.rustls {
|
||||
tls.root_store
|
||||
.add(&rustls_cert)
|
||||
.map_err(|_| Error::InvalidCertificate)?;
|
||||
}
|
||||
}
|
||||
if self.accept_invalid_certs {
|
||||
tls.dangerous()
|
||||
.set_certificate_verifier(Arc::new(InvalidCertsVerifier {}));
|
||||
}
|
||||
|
||||
tls.root_store.add_server_trust_anchors(&TLS_SERVER_ROOTS);
|
||||
Ok(TlsParameters {
|
||||
connector: InnerTlsParameters::RustlsTls(tls),
|
||||
domain: self.domain,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum InnerTlsParameters {
|
||||
#[cfg(feature = "native-tls")]
|
||||
NativeTls(TlsConnector),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
RustlsTls(ClientConfig),
|
||||
}
|
||||
|
||||
impl TlsParameters {
|
||||
/// Creates a new `TlsParameters` using native-tls or rustls
|
||||
/// depending on which one is available
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
pub fn new(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build()
|
||||
}
|
||||
|
||||
pub fn builder(domain: String) -> TlsParametersBuilder {
|
||||
TlsParametersBuilder::new(domain)
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using native-tls
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
pub fn new_native(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build_native()
|
||||
}
|
||||
|
||||
/// Creates a new `TlsParameters` using rustls
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
pub fn new_rustls(domain: String) -> Result<Self, Error> {
|
||||
TlsParametersBuilder::new(domain).build_rustls()
|
||||
}
|
||||
|
||||
pub fn domain(&self) -> &str {
|
||||
&self.domain
|
||||
}
|
||||
}
|
||||
|
||||
/// A client certificate that can be used with [`TlsParametersBuilder::add_root_certificate`]
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
pub struct Certificate {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls::Certificate,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: Vec<rustls::Certificate>,
|
||||
}
|
||||
|
||||
impl Certificate {
|
||||
/// Create a `Certificate` from a DER encoded certificate
|
||||
pub fn from_der(der: Vec<u8>) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert =
|
||||
native_tls::Certificate::from_der(&der).map_err(|_| Error::InvalidCertificate)?;
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: vec![rustls::Certificate(der)],
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a `Certificate` from a PEM encoded certificate
|
||||
pub fn from_pem(pem: &[u8]) -> Result<Self, Error> {
|
||||
#[cfg(feature = "native-tls")]
|
||||
let native_tls_cert =
|
||||
native_tls::Certificate::from_pem(pem).map_err(|_| Error::InvalidCertificate)?;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let rustls_cert = {
|
||||
use rustls::internal::pemfile;
|
||||
use std::io::Cursor;
|
||||
|
||||
let mut pem = Cursor::new(pem);
|
||||
pemfile::certs(&mut pem).map_err(|_| Error::InvalidCertificate)?
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
#[cfg(feature = "native-tls")]
|
||||
native_tls: native_tls_cert,
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
rustls: rustls_cert,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
struct InvalidCertsVerifier;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl ServerCertVerifier for InvalidCertsVerifier {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_roots: &RootCertStore,
|
||||
_presented_certs: &[rustls::Certificate],
|
||||
_dns_name: DNSNameRef<'_>,
|
||||
_ocsp_response: &[u8],
|
||||
) -> Result<ServerCertVerified, TLSError> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
374
src/transport/smtp/commands.rs
Normal file
374
src/transport/smtp/commands.rs
Normal file
@@ -0,0 +1,374 @@
|
||||
//! SMTP commands
|
||||
|
||||
use crate::{
|
||||
transport::smtp::{
|
||||
authentication::{Credentials, Mechanism},
|
||||
error::Error,
|
||||
extension::{ClientId, MailParameter, RcptParameter},
|
||||
response::Response,
|
||||
},
|
||||
Address,
|
||||
};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Ehlo {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for Ehlo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ehlo {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> Ehlo {
|
||||
Ehlo { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Starttls;
|
||||
|
||||
impl Display for Starttls {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Mail {
|
||||
sender: Option<Address>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for Mail {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|s| s.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Mail {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<Address>, parameters: Vec<MailParameter>) -> Mail {
|
||||
Mail { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rcpt {
|
||||
recipient: Address,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for Rcpt {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Rcpt {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: Address, parameters: Vec<RcptParameter>) -> Rcpt {
|
||||
Rcpt {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Data;
|
||||
|
||||
impl Display for Data {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Quit;
|
||||
|
||||
impl Display for Quit {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Noop;
|
||||
|
||||
impl Display for Noop {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Help {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Help {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if let Some(argument) = &self.argument {
|
||||
write!(f, " {}", argument)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Help {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> Help {
|
||||
Help { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Vrfy {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for Vrfy {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl Vrfy {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> Vrfy {
|
||||
Vrfy { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Expn {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for Expn {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl Expn {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> Expn {
|
||||
Expn { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Rset;
|
||||
|
||||
impl Display for Rset {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Auth {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Auth {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let encoded_response = self.response.as_ref().map(base64::encode);
|
||||
|
||||
if self.mechanism.supports_initial_response() {
|
||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||
} else {
|
||||
match encoded_response {
|
||||
Some(response) => f.write_str(&response)?,
|
||||
None => write!(f, "AUTH {}", self.mechanism)?,
|
||||
}
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<Auth, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_deref())?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an AUTH command from a response that needs to be a
|
||||
/// valid challenge (with 334 response code)
|
||||
pub fn new_from_response(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<Auth, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
#[cfg(feature = "tracing")]
|
||||
tracing::debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(Auth {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::transport::smtp::extension::MailBodyParameter;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = Address::from_str("test@example.com").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!("{}", Ehlo::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Mail::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Mail::new(None, vec![])), "MAIL FROM:<>\r\n");
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mail::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Mail::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
MailParameter::Body(MailBodyParameter::EightBitMime),
|
||||
mail_parameter,
|
||||
],
|
||||
)
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Rcpt::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Rcpt::new(email, vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Quit), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", Data), "DATA\r\n");
|
||||
assert_eq!(format!("{}", Noop), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", Help::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", Help::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Vrfy::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Expn::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", Rset), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Auth::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
Auth::new(Mechanism::Login, credentials, None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
162
src/transport/smtp/error.rs
Normal file
162
src/transport/smtp/error.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use crate::transport::smtp::response::{Response, Severity};
|
||||
use base64::DecodeError;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
io,
|
||||
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 UTF8 in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
Resolution,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
#[cfg(feature = "native-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "native-tls")))]
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::error::ErrorKind),
|
||||
/// Invalid hostname
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls-tls")))]
|
||||
InvalidDNSName(webpki::InvalidDNSNameError),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "native-tls", feature = "rustls-tls"))))]
|
||||
InvalidCertificate,
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
Pool(r2d2::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
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) => fmt.write_str(
|
||||
err.first_line()
|
||||
.unwrap_or("transient error during SMTP transaction"),
|
||||
),
|
||||
Permanent(ref err) => fmt.write_str(
|
||||
err.first_line()
|
||||
.unwrap_or("permanent error during SMTP transaction"),
|
||||
),
|
||||
ResponseParsing(err) => fmt.write_str(err),
|
||||
ChallengeParsing(ref err) => err.fmt(fmt),
|
||||
Utf8Parsing(ref err) => err.fmt(fmt),
|
||||
Resolution => fmt.write_str("could not resolve hostname"),
|
||||
Client(err) => fmt.write_str(err),
|
||||
Io(ref err) => err.fmt(fmt),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => err.fmt(fmt),
|
||||
Parsing(ref err) => fmt.write_str(err.description()),
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
InvalidDNSName(ref err) => err.fmt(fmt),
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
InvalidCertificate => fmt.write_str("invalid certificate"),
|
||||
#[cfg(feature = "r2d2")]
|
||||
Pool(ref err) => err.fmt(fmt),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
Io(ref err) => Some(&*err),
|
||||
#[cfg(feature = "native-tls")]
|
||||
Tls(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::Err<nom::error::Error<&str>>> for Error {
|
||||
fn from(err: nom::Err<nom::error::Error<&str>>) -> Error {
|
||||
Parsing(match err {
|
||||
nom::Err::Incomplete(_) => nom::error::ErrorKind::Complete,
|
||||
nom::Err::Failure(e) => e.code,
|
||||
nom::Err::Error(e) => e.code,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Error {
|
||||
ChallengeParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
impl From<webpki::InvalidDNSNameError> for Error {
|
||||
fn from(err: webpki::InvalidDNSNameError) -> Error {
|
||||
InvalidDNSName(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
impl From<r2d2::Error> for Error {
|
||||
fn from(err: r2d2::Error) -> Error {
|
||||
Pool(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)
|
||||
}
|
||||
}
|
||||
415
src/transport/smtp/extension.rs
Normal file
415
src/transport/smtp/extension.rs
Normal file
@@ -0,0 +1,415 @@
|
||||
//! ESMTP features
|
||||
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism, error::Error, response::Response, util::XText,
|
||||
};
|
||||
use std::{
|
||||
collections::HashSet,
|
||||
fmt::{self, Display, Formatter},
|
||||
net::{Ipv4Addr, Ipv6Addr},
|
||||
result::Result,
|
||||
};
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientId {
|
||||
/// A fully-qualified domain name
|
||||
Domain(String),
|
||||
/// An IPv4 address
|
||||
Ipv4(Ipv4Addr),
|
||||
/// An IPv6 address
|
||||
Ipv6(Ipv6Addr),
|
||||
}
|
||||
|
||||
const LOCALHOST_CLIENT: ClientId = ClientId::Ipv4(Ipv4Addr::new(127, 0, 0, 1));
|
||||
|
||||
impl Default for ClientId {
|
||||
fn default() -> Self {
|
||||
// https://tools.ietf.org/html/rfc5321#section-4.1.4
|
||||
//
|
||||
// The SMTP client MUST, if possible, ensure that the domain parameter
|
||||
// to the EHLO command is a primary host name as specified for this
|
||||
// command in Section 2.3.5. If this is not possible (e.g., when the
|
||||
// client's address is dynamically assigned and the client does not have
|
||||
// an obvious name), an address literal SHOULD be substituted for the
|
||||
// domain name.
|
||||
#[cfg(feature = "hostname")]
|
||||
{
|
||||
hostname::get()
|
||||
.ok()
|
||||
.and_then(|s| s.into_string().map(Self::Domain).ok())
|
||||
.unwrap_or(LOCALHOST_CLIENT)
|
||||
}
|
||||
#[cfg(not(feature = "hostname"))]
|
||||
LOCALHOST_CLIENT
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Domain(ref value) => f.write_str(value),
|
||||
Self::Ipv4(ref value) => write!(f, "[{}]", value),
|
||||
Self::Ipv6(ref value) => write!(f, "[IPv6:{}]", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
#[deprecated(since = "0.10.0", note = "Please use ClientId::Domain(domain) instead")]
|
||||
/// Creates a new `ClientId` from a fully qualified domain name
|
||||
pub fn new(domain: String) -> Self {
|
||||
Self::Domain(domain)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Extension {
|
||||
/// 8BITMIME keyword
|
||||
///
|
||||
/// RFC 6152: https://tools.ietf.org/html/rfc6152
|
||||
EightBitMime,
|
||||
/// SMTPUTF8 keyword
|
||||
///
|
||||
/// RFC 6531: https://tools.ietf.org/html/rfc6531
|
||||
SmtpUtfEight,
|
||||
/// STARTTLS keyword
|
||||
///
|
||||
/// RFC 2487: https://tools.ietf.org/html/rfc2487
|
||||
StartTls,
|
||||
/// AUTH mechanism
|
||||
Authentication(Mechanism),
|
||||
}
|
||||
|
||||
impl Display for Extension {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
Extension::EightBitMime => f.write_str("8BITMIME"),
|
||||
Extension::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
Extension::StartTls => f.write_str("STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Default)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ServerInfo {
|
||||
/// Server name
|
||||
///
|
||||
/// The name given in the server banner
|
||||
pub name: String,
|
||||
/// ESMTP features supported by the server
|
||||
///
|
||||
/// It contains the features supported by the server and known by the `Extension` module.
|
||||
pub features: HashSet<Extension>,
|
||||
}
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let features = if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.features)
|
||||
};
|
||||
write!(f, "{} with {}", self.name, features)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerInfo {
|
||||
/// Parses a EHLO response to create a `ServerInfo`
|
||||
pub fn from_response(response: &Response) -> Result<ServerInfo, Error> {
|
||||
let name = match response.first_word() {
|
||||
Some(name) => name,
|
||||
None => return Err(Error::ResponseParsing("Could not read server name")),
|
||||
};
|
||||
|
||||
let mut features: HashSet<Extension> = HashSet::new();
|
||||
|
||||
for line in response.message.as_slice() {
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut split = line.split_whitespace();
|
||||
match split.next().unwrap() {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
"SMTPUTF8" => {
|
||||
features.insert(Extension::SmtpUtfEight);
|
||||
}
|
||||
"STARTTLS" => {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for mechanism in split {
|
||||
match mechanism {
|
||||
"PLAIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Plain));
|
||||
}
|
||||
"LOGIN" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Login));
|
||||
}
|
||||
"XOAUTH2" => {
|
||||
features.insert(Extension::Authentication(Mechanism::Xoauth2));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
Ok(ServerInfo {
|
||||
name: name.to_string(),
|
||||
features,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_feature(&self, keyword: Extension) -> bool {
|
||||
self.features.contains(&keyword)
|
||||
}
|
||||
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_auth_mechanism(&self, mechanism: Mechanism) -> bool {
|
||||
self.features
|
||||
.contains(&Extension::Authentication(mechanism))
|
||||
}
|
||||
|
||||
/// Gets a compatible mechanism from list
|
||||
pub fn get_auth_mechanism(&self, mechanisms: &[Mechanism]) -> Option<Mechanism> {
|
||||
for mechanism in mechanisms {
|
||||
if self.supports_auth_mechanism(*mechanism) {
|
||||
return Some(*mechanism);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// A `MAIL FROM` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailParameter {
|
||||
/// `BODY` parameter
|
||||
Body(MailBodyParameter),
|
||||
/// `SIZE` parameter
|
||||
Size(usize),
|
||||
/// `SMTPUTF8` parameter
|
||||
SmtpUtfEight,
|
||||
/// Custom parameter
|
||||
Other {
|
||||
/// Parameter keyword
|
||||
keyword: String,
|
||||
/// Parameter value
|
||||
value: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for MailParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailParameter::Body(ref value) => write!(f, "BODY={}", value),
|
||||
MailParameter::Size(size) => write!(f, "SIZE={}", size),
|
||||
MailParameter::SmtpUtfEight => f.write_str("SMTPUTF8"),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
MailParameter::Other {
|
||||
ref keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Values for the `BODY` parameter to `MAIL FROM`
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum MailBodyParameter {
|
||||
/// `7BIT`
|
||||
SevenBit,
|
||||
/// `8BITMIME`
|
||||
EightBitMime,
|
||||
}
|
||||
|
||||
impl Display for MailBodyParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
MailBodyParameter::SevenBit => f.write_str("7BIT"),
|
||||
MailBodyParameter::EightBitMime => f.write_str("8BITMIME"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `RCPT TO` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum RcptParameter {
|
||||
/// Custom parameter
|
||||
Other {
|
||||
/// Parameter keyword
|
||||
keyword: String,
|
||||
/// Parameter value
|
||||
value: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Display for RcptParameter {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: Some(ref value),
|
||||
} => write!(f, "{}={}", keyword, XText(value)),
|
||||
RcptParameter::Other {
|
||||
ref keyword,
|
||||
value: None,
|
||||
} => f.write_str(keyword),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use super::*;
|
||||
use crate::transport::smtp::{
|
||||
authentication::Mechanism,
|
||||
response::{Category, Code, Detail, Response, Severity},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::Domain("test".to_string())),
|
||||
"test".to_string()
|
||||
);
|
||||
assert_eq!(format!("{}", LOCALHOST_CLIENT), "[127.0.0.1]".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extension_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Extension::EightBitMime),
|
||||
"8BITMIME".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Extension::Authentication(Mechanism::Plain)),
|
||||
"AUTH PLAIN".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo_fmt() {
|
||||
let mut eightbitmime = HashSet::new();
|
||||
assert!(eightbitmime.insert(Extension::EightBitMime));
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: eightbitmime,
|
||||
}
|
||||
),
|
||||
"name with {EightBitMime}".to_string()
|
||||
);
|
||||
|
||||
let empty = HashSet::new();
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: empty,
|
||||
}
|
||||
),
|
||||
"name with no supported features".to_string()
|
||||
);
|
||||
|
||||
let mut plain = HashSet::new();
|
||||
assert!(plain.insert(Extension::Authentication(Mechanism::Plain)));
|
||||
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
ServerInfo {
|
||||
name: "name".to_string(),
|
||||
features: plain,
|
||||
}
|
||||
),
|
||||
"name with {Authentication(Plain)}".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serverinfo() {
|
||||
let response = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
let mut features = HashSet::new();
|
||||
assert!(features.insert(Extension::EightBitMime));
|
||||
|
||||
let server_info = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response).unwrap(), server_info);
|
||||
|
||||
assert!(server_info.supports_feature(Extension::EightBitMime));
|
||||
assert!(!server_info.supports_feature(Extension::StartTls));
|
||||
|
||||
let response2 = Response::new(
|
||||
Code::new(
|
||||
Severity::PositiveCompletion,
|
||||
Category::Unspecified4,
|
||||
Detail::One,
|
||||
),
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5 XOAUTH2 OTHER".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
let mut features2 = HashSet::new();
|
||||
assert!(features2.insert(Extension::EightBitMime));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Plain),));
|
||||
assert!(features2.insert(Extension::Authentication(Mechanism::Xoauth2),));
|
||||
|
||||
let server_info2 = ServerInfo {
|
||||
name: "me".to_string(),
|
||||
features: features2,
|
||||
};
|
||||
|
||||
assert_eq!(ServerInfo::from_response(&response2).unwrap(), server_info2);
|
||||
|
||||
assert!(server_info2.supports_feature(Extension::EightBitMime));
|
||||
assert!(server_info2.supports_auth_mechanism(Mechanism::Plain));
|
||||
assert!(!server_info2.supports_feature(Extension::StartTls));
|
||||
}
|
||||
}
|
||||
247
src/transport/smtp/mod.rs
Normal file
247
src/transport/smtp/mod.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
//! The SMTP transport sends emails using the SMTP protocol.
|
||||
//!
|
||||
//! This SMTP client follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
|
||||
//! application to a relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC compliance checks.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](https://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](https://tools.ietf.org/html/rfc2487))
|
||||
//!
|
||||
//! #### SMTP Transport
|
||||
//!
|
||||
//! This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
//!
|
||||
//! It is designed to be:
|
||||
//!
|
||||
//! * Secured: connections are encrypted by default
|
||||
//! * Modern: unicode support for email contents and sender/recipient addresses when compatible
|
||||
//! * Fast: supports connection reuse and pooling
|
||||
//!
|
||||
//! This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
//! emails directly to the destination server.
|
||||
//!
|
||||
//! The relay server can be the local email server, a specific host or a third-party service.
|
||||
//!
|
||||
//! #### Simple example
|
||||
//!
|
||||
//! This is the most basic example of usage:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! # #[cfg(all(feature = "builder", any(feature = "native-tls", feature = "rustls-tls")))]
|
||||
//! # fn test() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! use lettre::{Message, Transport, SmtpTransport};
|
||||
//!
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! // Create TLS transport on port 465
|
||||
//! let sender = SmtpTransport::relay("smtp.example.com")
|
||||
//! .expect("relay valid")
|
||||
//! .build();
|
||||
//! // Send the email via remote relay
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
//! #### Complete example
|
||||
//!
|
||||
//! ```todo
|
||||
//! # #[cfg(feature = "smtp-transport")]
|
||||
//! # {
|
||||
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
//! use lettre::{Email, Envelope, Transport, SmtpClient};
|
||||
//! use lettre::transport::smtp::extension::ClientId;
|
||||
//!
|
||||
//! let email_1 = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string())?),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string())?],
|
||||
//! )?,
|
||||
//! "id1".to_string(),
|
||||
//! "Hello world".to_string().into_bytes(),
|
||||
//! );
|
||||
//!
|
||||
//! let email_2 = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string())?),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string())?],
|
||||
//! )?,
|
||||
//! "id2".to_string(),
|
||||
//! "Hello world a second time".to_string().into_bytes(),
|
||||
//! );
|
||||
//!
|
||||
//! // Connect to a remote server on a custom port
|
||||
//! let mut mailer = SmtpClient::new_simple("server.tld")?
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name(ClientId::Domain("my.hostname.tld".to_string()))
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials(Credentials::new("username".to_string(), "password".to_string()))
|
||||
//! // Enable SMTPUTF8 if the server supports it
|
||||
//! .smtp_utf8(true)
|
||||
//! // Configure expected authentication mechanism
|
||||
//! .authentication_mechanism(Mechanism::Plain)
|
||||
//! // Enable connection reuse
|
||||
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited).transport();
|
||||
//!
|
||||
//! let result_1 = mailer.send(&email_1);
|
||||
//! assert!(result_1.is_ok());
|
||||
//!
|
||||
//! // The second email will use the same connection
|
||||
//! let result_2 = mailer.send(&email_2);
|
||||
//! assert!(result_2.is_ok());
|
||||
//!
|
||||
//! // Explicitly close the SMTP transaction as we enabled connection reuse
|
||||
//! mailer.close();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! You can specify custom TLS settings:
|
||||
//!
|
||||
//! ```todo
|
||||
//! # #[cfg(feature = "native-tls")]
|
||||
//! # {
|
||||
//! use lettre::{
|
||||
//! ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
//! Email, SmtpClient, Transport,
|
||||
//! };
|
||||
//! use lettre::transport::smtp::authentication::{Credentials, Mechanism};
|
||||
//! use lettre::transport::smtp::ConnectionReuseParameters;
|
||||
//! use native_tls::{Protocol, TlsConnector};
|
||||
//!
|
||||
//! let email = Email::new(
|
||||
//! Envelope::new(
|
||||
//! Some(EmailAddress::new("user@localhost".to_string())?),
|
||||
//! vec![EmailAddress::new("root@localhost".to_string())?],
|
||||
//! )?,
|
||||
//! "message_id".to_string(),
|
||||
//! "Hello world".to_string().into_bytes(),
|
||||
//! );
|
||||
//!
|
||||
//! let mut tls_builder = TlsConnector::builder();
|
||||
//! tls_builder.min_protocol_version(Some(Protocol::Tlsv10));
|
||||
//! let tls_parameters =
|
||||
//! ClientTlsParameters::new(
|
||||
//! "smtp.example.com".to_string(),
|
||||
//! tls_builder.build()?
|
||||
//! );
|
||||
//!
|
||||
//! let mut mailer = SmtpClient::new(
|
||||
//! ("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
|
||||
//! )?
|
||||
//! .authentication_mechanism(Mechanism::Login)
|
||||
//! .credentials(Credentials::new(
|
||||
//! "example_username".to_string(), "example_password".to_string()
|
||||
//! ))
|
||||
//! .connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
//! .transport();
|
||||
//!
|
||||
//! let result = mailer.send(&email);
|
||||
//!
|
||||
//! assert!(result.is_ok());
|
||||
//!
|
||||
//! mailer.close();
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
pub use self::async_transport::AsyncStd1Connector;
|
||||
#[cfg(feature = "tokio02")]
|
||||
pub use self::async_transport::Tokio02Connector;
|
||||
#[cfg(feature = "tokio1")]
|
||||
pub use self::async_transport::Tokio1Connector;
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
pub use self::async_transport::{
|
||||
AsyncSmtpConnector, AsyncSmtpTransport, AsyncSmtpTransportBuilder,
|
||||
};
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub use self::pool::PoolConfig;
|
||||
#[cfg(feature = "r2d2")]
|
||||
pub(crate) use self::transport::SmtpClient;
|
||||
pub use self::{
|
||||
error::Error,
|
||||
transport::{SmtpTransport, SmtpTransportBuilder},
|
||||
};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use crate::transport::smtp::client::TlsParameters;
|
||||
use crate::transport::smtp::{
|
||||
authentication::{Credentials, Mechanism, DEFAULT_MECHANISMS},
|
||||
client::SmtpConnection,
|
||||
extension::ClientId,
|
||||
response::Response,
|
||||
};
|
||||
use client::Tls;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(any(feature = "tokio02", feature = "tokio1", feature = "async-std1"))]
|
||||
mod async_transport;
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "r2d2")]
|
||||
mod pool;
|
||||
pub mod response;
|
||||
mod transport;
|
||||
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
|
||||
///
|
||||
/// https://tools.ietf.org/html/rfc8314
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// Default timeout
|
||||
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
struct SmtpInfo {
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Server we are connecting to
|
||||
server: String,
|
||||
/// Port to connect to
|
||||
port: u16,
|
||||
/// TLS security configuration
|
||||
tls: Tls,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication: Vec<Mechanism>,
|
||||
/// Credentials
|
||||
credentials: Option<Credentials>,
|
||||
/// Define network timeout
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl Default for SmtpInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
server: "localhost".to_string(),
|
||||
port: SMTP_PORT,
|
||||
hello_name: ClientId::default(),
|
||||
credentials: None,
|
||||
authentication: DEFAULT_MECHANISMS.into(),
|
||||
timeout: Some(DEFAULT_TIMEOUT),
|
||||
tls: Tls::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/transport/smtp/pool.rs
Normal file
90
src/transport/smtp/pool.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::transport::smtp::{client::SmtpConnection, error::Error, SmtpClient};
|
||||
|
||||
use r2d2::{ManageConnection, Pool};
|
||||
|
||||
/// Configuration for a connection pool
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(missing_copy_implementations)]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub struct PoolConfig {
|
||||
min_idle: u32,
|
||||
max_size: u32,
|
||||
connection_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
}
|
||||
|
||||
impl PoolConfig {
|
||||
/// Minimum number of idle connections
|
||||
///
|
||||
/// Defaults to `0`
|
||||
pub fn min_idle(mut self, min_idle: u32) -> Self {
|
||||
self.min_idle = min_idle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum number of pooled connections
|
||||
///
|
||||
/// Defaults to `10`
|
||||
pub fn max_size(mut self, max_size: u32) -> Self {
|
||||
self.min_idle = max_size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection timeout
|
||||
///
|
||||
/// Defaults to `30 seconds`
|
||||
pub fn connection_timeout(mut self, connection_timeout: Duration) -> Self {
|
||||
self.connection_timeout = connection_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Connection idle timeout
|
||||
///
|
||||
/// Defaults to `60 seconds`
|
||||
pub fn idle_timeout(mut self, idle_timeout: Duration) -> Self {
|
||||
self.idle_timeout = idle_timeout;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build<C: ManageConnection>(&self, client: C) -> Pool<C> {
|
||||
Pool::builder()
|
||||
.min_idle(Some(self.min_idle))
|
||||
.max_size(self.max_size)
|
||||
.connection_timeout(self.connection_timeout)
|
||||
.idle_timeout(Some(self.idle_timeout))
|
||||
.build_unchecked(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
min_idle: 0,
|
||||
max_size: 10,
|
||||
connection_timeout: Duration::from_secs(30),
|
||||
idle_timeout: Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpClient {
|
||||
type Connection = SmtpConnection;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
self.connection()
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.test_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.has_broken()
|
||||
}
|
||||
}
|
||||
565
src/transport/smtp/response.rs
Normal file
565
src/transport/smtp/response.rs
Normal file
@@ -0,0 +1,565 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use crate::transport::smtp::Error;
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::streaming::{tag, take_until},
|
||||
combinator::{complete, map},
|
||||
multi::many0,
|
||||
sequence::{preceded, tuple},
|
||||
IResult,
|
||||
};
|
||||
use std::{
|
||||
fmt::{Display, Formatter, Result},
|
||||
result,
|
||||
str::FromStr,
|
||||
string::ToString,
|
||||
};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Severity {
|
||||
/// 2yx
|
||||
PositiveCompletion = 2,
|
||||
/// 3yz
|
||||
PositiveIntermediate = 3,
|
||||
/// 4yz
|
||||
TransientNegativeCompletion = 4,
|
||||
/// 5yz
|
||||
PermanentNegativeCompletion = 5,
|
||||
}
|
||||
|
||||
impl Display for Severity {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Second digit
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Category {
|
||||
/// x0z
|
||||
Syntax = 0,
|
||||
/// x1z
|
||||
Information = 1,
|
||||
/// x2z
|
||||
Connections = 2,
|
||||
/// x3z
|
||||
Unspecified3 = 3,
|
||||
/// x4z
|
||||
Unspecified4 = 4,
|
||||
/// x5z
|
||||
MailSystem = 5,
|
||||
}
|
||||
|
||||
impl Display for Category {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// The detail digit of a response code (third digit)
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Detail {
|
||||
#[allow(missing_docs)]
|
||||
Zero = 0,
|
||||
#[allow(missing_docs)]
|
||||
One = 1,
|
||||
#[allow(missing_docs)]
|
||||
Two = 2,
|
||||
#[allow(missing_docs)]
|
||||
Three = 3,
|
||||
#[allow(missing_docs)]
|
||||
Four = 4,
|
||||
#[allow(missing_docs)]
|
||||
Five = 5,
|
||||
#[allow(missing_docs)]
|
||||
Six = 6,
|
||||
#[allow(missing_docs)]
|
||||
Seven = 7,
|
||||
#[allow(missing_docs)]
|
||||
Eight = 8,
|
||||
#[allow(missing_docs)]
|
||||
Nine = 9,
|
||||
}
|
||||
|
||||
impl Display for Detail {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}", *self as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a 3 digit SMTP response code
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Code {
|
||||
/// First digit of the response code
|
||||
pub severity: Severity,
|
||||
/// Second digit of the response code
|
||||
pub category: Category,
|
||||
/// Third digit
|
||||
pub detail: Detail,
|
||||
}
|
||||
|
||||
impl Display for Code {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
write!(f, "{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
impl Code {
|
||||
/// Creates a new `Code` structure
|
||||
pub fn new(severity: Severity, category: Category, detail: Detail) -> Code {
|
||||
Code {
|
||||
severity,
|
||||
category,
|
||||
detail,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an SMTP reply, with separated code and message
|
||||
///
|
||||
/// The text message is optional, only the code is mandatory
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Response {
|
||||
/// Response code
|
||||
pub code: Code,
|
||||
/// Server response string (optional)
|
||||
/// Handle multiline responses
|
||||
pub message: Vec<String>,
|
||||
}
|
||||
|
||||
impl FromStr for Response {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, Error> {
|
||||
parse_response(s).map(|(_, r)| r).map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response`
|
||||
pub fn new(code: Code, message: Vec<String>) -> Response {
|
||||
Response { code, message }
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(&self) -> bool {
|
||||
matches!(
|
||||
self.code.severity,
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate
|
||||
)
|
||||
}
|
||||
|
||||
/// Tests code equality
|
||||
pub fn has_code(&self, code: u16) -> bool {
|
||||
self.code.to_string() == code.to_string()
|
||||
}
|
||||
|
||||
/// Returns only the first word of the message if possible
|
||||
pub fn first_word(&self) -> Option<&str> {
|
||||
self.message
|
||||
.first()
|
||||
.and_then(|line| line.split_whitespace().next())
|
||||
}
|
||||
|
||||
/// Returns only the line of the message if possible
|
||||
pub fn first_line(&self) -> Option<&str> {
|
||||
self.message.first().map(String::as_str)
|
||||
}
|
||||
}
|
||||
|
||||
// Parsers (originally from tokio-smtp)
|
||||
|
||||
fn parse_code(i: &str) -> IResult<&str, Code> {
|
||||
let (i, severity) = parse_severity(i)?;
|
||||
let (i, category) = parse_category(i)?;
|
||||
let (i, detail) = parse_detail(i)?;
|
||||
Ok((
|
||||
i,
|
||||
Code {
|
||||
severity,
|
||||
category,
|
||||
detail,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_severity(i: &str) -> IResult<&str, Severity> {
|
||||
alt((
|
||||
map(tag("2"), |_| Severity::PositiveCompletion),
|
||||
map(tag("3"), |_| Severity::PositiveIntermediate),
|
||||
map(tag("4"), |_| Severity::TransientNegativeCompletion),
|
||||
map(tag("5"), |_| Severity::PermanentNegativeCompletion),
|
||||
))(i)
|
||||
}
|
||||
|
||||
fn parse_category(i: &str) -> IResult<&str, Category> {
|
||||
alt((
|
||||
map(tag("0"), |_| Category::Syntax),
|
||||
map(tag("1"), |_| Category::Information),
|
||||
map(tag("2"), |_| Category::Connections),
|
||||
map(tag("3"), |_| Category::Unspecified3),
|
||||
map(tag("4"), |_| Category::Unspecified4),
|
||||
map(tag("5"), |_| Category::MailSystem),
|
||||
))(i)
|
||||
}
|
||||
|
||||
fn parse_detail(i: &str) -> IResult<&str, Detail> {
|
||||
alt((
|
||||
map(tag("0"), |_| Detail::Zero),
|
||||
map(tag("1"), |_| Detail::One),
|
||||
map(tag("2"), |_| Detail::Two),
|
||||
map(tag("3"), |_| Detail::Three),
|
||||
map(tag("4"), |_| Detail::Four),
|
||||
map(tag("5"), |_| Detail::Five),
|
||||
map(tag("6"), |_| Detail::Six),
|
||||
map(tag("7"), |_| Detail::Seven),
|
||||
map(tag("8"), |_| Detail::Eight),
|
||||
map(tag("9"), |_| Detail::Nine),
|
||||
))(i)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_response(i: &str) -> IResult<&str, Response> {
|
||||
let (i, lines) = many0(tuple((
|
||||
parse_code,
|
||||
preceded(tag("-"), take_until("\r\n")),
|
||||
tag("\r\n"),
|
||||
)))(i)?;
|
||||
let (i, (last_code, last_line)) =
|
||||
tuple((parse_code, preceded(tag(" "), take_until("\r\n"))))(i)?;
|
||||
let (i, _) = complete(tag("\r\n"))(i)?;
|
||||
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(code, _, _)| code == last_code) {
|
||||
return Err(nom::Err::Failure(nom::error::Error::new(
|
||||
"",
|
||||
nom::error::ErrorKind::Not,
|
||||
)));
|
||||
}
|
||||
|
||||
// Extract text from lines, and append last line.
|
||||
let mut lines: Vec<String> = lines.into_iter().map(|(_, text, _)| text.into()).collect();
|
||||
lines.push(last_line.into());
|
||||
|
||||
Ok((
|
||||
i,
|
||||
Response {
|
||||
code: last_code,
|
||||
message: lines,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_severity_fmt() {
|
||||
assert_eq!(format!("{}", Severity::PositiveCompletion), "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_fmt() {
|
||||
assert_eq!(format!("{}", Category::Unspecified4), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_new() {
|
||||
assert_eq!(
|
||||
Code::new(
|
||||
Severity::TransientNegativeCompletion,
|
||||
Category::Connections,
|
||||
Detail::Zero,
|
||||
),
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail::Zero,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_code_display() {
|
||||
let code = Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::Connections,
|
||||
detail: Detail::One,
|
||||
};
|
||||
|
||||
assert_eq!(code.to_string(), "421");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_from_str() {
|
||||
let raw_response = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||
assert_eq!(
|
||||
raw_response.parse::<Response>().unwrap(),
|
||||
Response {
|
||||
code: Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
message: vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
"AUTH PLAIN CRAM-MD5".to_string(),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
let wrong_code = "2506-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250 AUTH PLAIN CRAM-MD5\r\n";
|
||||
assert!(wrong_code.parse::<Response>().is_err());
|
||||
|
||||
let wrong_end = "250-me\r\n250-8BITMIME\r\n250-SIZE 42\r\n250-AUTH PLAIN CRAM-MD5\r\n";
|
||||
assert!(wrong_end.parse::<Response>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_is_positive() {
|
||||
assert!(Response::new(
|
||||
Code {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.is_positive());
|
||||
assert!(!Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::Zero,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.is_positive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_has_code() {
|
||||
assert!(Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.has_code(451));
|
||||
assert!(!Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.has_code(251));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_first_word() {
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.first_word(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
)
|
||||
.first_word(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_incomplete() {
|
||||
let raw_response = "250-smtp.example.org\r\n";
|
||||
let res = parse_response(raw_response);
|
||||
match res {
|
||||
Err(nom::Err::Incomplete(_)) => {}
|
||||
_ => panic!("Expected incomplete response, got {:?}", res),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_first_line() {
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.first_line(),
|
||||
Some("me")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![
|
||||
"me mo".to_string(),
|
||||
"8BITMIME".to_string(),
|
||||
"SIZE 42".to_string(),
|
||||
],
|
||||
)
|
||||
.first_line(),
|
||||
Some("me mo")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![],
|
||||
)
|
||||
.first_line(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec![" ".to_string()],
|
||||
)
|
||||
.first_line(),
|
||||
Some(" ")
|
||||
);
|
||||
assert_eq!(
|
||||
Response::new(
|
||||
Code {
|
||||
severity: Severity::TransientNegativeCompletion,
|
||||
category: Category::MailSystem,
|
||||
detail: Detail::One,
|
||||
},
|
||||
vec!["".to_string()],
|
||||
)
|
||||
.first_line(),
|
||||
Some("")
|
||||
);
|
||||
}
|
||||
}
|
||||
230
src/transport/smtp/transport.rs
Normal file
230
src/transport/smtp/transport.rs
Normal file
@@ -0,0 +1,230 @@
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
use r2d2::Pool;
|
||||
|
||||
#[cfg(feature = "r2d2")]
|
||||
use super::PoolConfig;
|
||||
use super::{ClientId, Credentials, Error, Mechanism, Response, SmtpConnection, SmtpInfo};
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
use super::{Tls, TlsParameters, SUBMISSIONS_PORT, SUBMISSION_PORT};
|
||||
use crate::address::Envelope;
|
||||
use crate::Transport;
|
||||
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: Pool<SmtpClient>,
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: SmtpClient,
|
||||
}
|
||||
|
||||
impl Transport for SmtpTransport {
|
||||
type Ok = Response;
|
||||
type Error = Error;
|
||||
|
||||
/// Sends an email
|
||||
fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
#[cfg(feature = "r2d2")]
|
||||
let mut conn = self.inner.get()?;
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
let mut conn = self.inner.connection()?;
|
||||
|
||||
let result = conn.send(envelope, email)?;
|
||||
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
conn.quit()?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpTransport {
|
||||
/// Simple and secure transport, using TLS connections to communicate with the SMTP server
|
||||
///
|
||||
/// The right option for most SMTP servers.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub fn relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSIONS_PORT)
|
||||
.tls(Tls::Wrapper(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Simple an secure transport, using STARTTLS to obtain encrypted connections
|
||||
///
|
||||
/// Alternative to [`SmtpTransport::relay`](#method.relay), for SMTP servers
|
||||
/// that don't take SMTPS connections.
|
||||
///
|
||||
/// Creates an encrypted transport over submissions port, by first connecting using
|
||||
/// an unencrypted connection and then upgrading it with STARTTLS. The provided
|
||||
/// domain is used to validate TLS certificates.
|
||||
///
|
||||
/// An error is returned if the connection can't be upgraded. No credentials
|
||||
/// or emails will be sent to the server, protecting from downgrade attacks.
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub fn starttls_relay(relay: &str) -> Result<SmtpTransportBuilder, Error> {
|
||||
let tls_parameters = TlsParameters::new(relay.into())?;
|
||||
|
||||
Ok(Self::builder_dangerous(relay)
|
||||
.port(SUBMISSION_PORT)
|
||||
.tls(Tls::Required(tls_parameters)))
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
///
|
||||
/// Shortcut for local unencrypted relay (typical local email daemon that will handle relaying)
|
||||
pub fn unencrypted_localhost() -> SmtpTransport {
|
||||
Self::builder_dangerous("localhost").build()
|
||||
}
|
||||
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No authentication
|
||||
/// * No TLS
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
/// * Port 25
|
||||
///
|
||||
/// Consider using [`SmtpTransport::relay`](#method.relay) or
|
||||
/// [`SmtpTransport::starttls_relay`](#method.starttls_relay) instead,
|
||||
/// if possible.
|
||||
pub fn builder_dangerous<T: Into<String>>(server: T) -> SmtpTransportBuilder {
|
||||
let new = SmtpInfo {
|
||||
server: server.into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
SmtpTransportBuilder {
|
||||
info: new,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool_config: PoolConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains client configuration.
|
||||
/// Instances of this struct can be created using functions of [`SmtpTransport`].
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpTransportBuilder {
|
||||
info: SmtpInfo,
|
||||
#[cfg(feature = "r2d2")]
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpTransportBuilder {
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> Self {
|
||||
self.info.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn credentials(mut self, credentials: Credentials) -> Self {
|
||||
self.info.credentials = Some(credentials);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication(mut self, mechanisms: Vec<Mechanism>) -> Self {
|
||||
self.info.authentication = mechanisms;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.info.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the port to use
|
||||
pub fn port(mut self, port: u16) -> Self {
|
||||
self.info.port = port;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the TLS settings to use
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
pub fn tls(mut self, tls: Tls) -> Self {
|
||||
self.info.tls = tls;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom configuration for the connection pool
|
||||
///
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
#[cfg(feature = "r2d2")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "r2d2")))]
|
||||
pub fn pool_config(mut self, pool_config: PoolConfig) -> Self {
|
||||
self.pool_config = pool_config;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the transport
|
||||
///
|
||||
/// If the `r2d2` feature is enabled an `Arc` wrapped pool is be created.
|
||||
/// Defaults can be found at [`PoolConfig`]
|
||||
pub fn build(self) -> SmtpTransport {
|
||||
let client = SmtpClient { info: self.info };
|
||||
SmtpTransport {
|
||||
#[cfg(feature = "r2d2")]
|
||||
inner: self.pool_config.build(client),
|
||||
#[cfg(not(feature = "r2d2"))]
|
||||
inner: client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Build client
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
info: SmtpInfo,
|
||||
}
|
||||
|
||||
impl SmtpClient {
|
||||
/// Creates a new connection directly usable to send emails
|
||||
///
|
||||
/// Handles encryption and authentication
|
||||
pub fn connection(&self) -> Result<SmtpConnection, Error> {
|
||||
#[allow(clippy::match_single_binding)]
|
||||
let tls_parameters = match self.info.tls {
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
Tls::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut conn = SmtpConnection::connect::<(&str, u16)>(
|
||||
(self.info.server.as_ref(), self.info.port),
|
||||
self.info.timeout,
|
||||
&self.info.hello_name,
|
||||
tls_parameters,
|
||||
)?;
|
||||
|
||||
#[cfg(any(feature = "native-tls", feature = "rustls-tls"))]
|
||||
match self.info.tls {
|
||||
Tls::Opportunistic(ref tls_parameters) => {
|
||||
if conn.can_starttls() {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
}
|
||||
Tls::Required(ref tls_parameters) => {
|
||||
conn.starttls(tls_parameters, &self.info.hello_name)?;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(credentials) = &self.info.credentials {
|
||||
conn.auth(&self.info.authentication, &credentials)?;
|
||||
}
|
||||
Ok(conn)
|
||||
}
|
||||
}
|
||||
48
src/transport/smtp/util.rs
Normal file
48
src/transport/smtp/util.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Utils for string manipulation
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct XText<'a>(pub &'a str);
|
||||
|
||||
impl<'a> Display for XText<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||
let mut rest = self.0;
|
||||
while let Some(idx) = rest.find(|c| c < '!' || c == '+' || c == '=') {
|
||||
let (start, end) = rest.split_at(idx);
|
||||
f.write_str(start)?;
|
||||
|
||||
let mut end_iter = end.char_indices();
|
||||
let (_, c) = end_iter.next().expect("char");
|
||||
write!(f, "+{:X}", c as u8)?;
|
||||
|
||||
if let Some((idx, _)) = end_iter.next() {
|
||||
rest = &end[idx..];
|
||||
} else {
|
||||
rest = "";
|
||||
}
|
||||
}
|
||||
f.write_str(rest)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::XText;
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
for (input, expect) in [
|
||||
("bjorn", "bjorn"),
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
assert_eq!(format!("{}", XText(input)), expect.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/transport/stub/mod.rs
Normal file
107
src/transport/stub/mod.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
//! #### Stub Transport
|
||||
//!
|
||||
//! The stub transport returns provided result and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
//! ```rust
|
||||
//! # #[cfg(feature = "builder")]
|
||||
//! # {
|
||||
//! use lettre::{Message, Transport};
|
||||
//! use lettre::transport::stub::StubTransport;
|
||||
//!
|
||||
//! # use std::error::Error;
|
||||
//! # fn main() -> Result<(), Box<dyn Error>> {
|
||||
//! let email = Message::builder()
|
||||
//! .from("NoBody <nobody@domain.tld>".parse()?)
|
||||
//! .reply_to("Yuin <yuin@domain.tld>".parse()?)
|
||||
//! .to("Hei <hei@domain.tld>".parse()?)
|
||||
//! .subject("Happy new year")
|
||||
//! .body(String::from("Be happy!"))?;
|
||||
//!
|
||||
//! let mut sender = StubTransport::new_ok();
|
||||
//! let result = sender.send(&email);
|
||||
//! assert!(result.is_ok());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::address::Envelope;
|
||||
#[cfg(feature = "async-std1")]
|
||||
use crate::AsyncStd1Transport;
|
||||
#[cfg(feature = "tokio02")]
|
||||
use crate::Tokio02Transport;
|
||||
use crate::Transport;
|
||||
#[cfg(any(feature = "async-std1", feature = "tokio02"))]
|
||||
use async_trait::async_trait;
|
||||
use std::{error::Error as StdError, fmt};
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct Error;
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("stub error")
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {}
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubTransport {
|
||||
response: Result<(), Error>,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given Result
|
||||
pub fn new(response: Result<(), Error>) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_ok() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns an error
|
||||
pub fn new_error() -> StubTransport {
|
||||
StubTransport {
|
||||
response: Err(Error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_trait]
|
||||
impl AsyncStd1Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[async_trait]
|
||||
impl Tokio02Transport for StubTransport {
|
||||
type Ok = ();
|
||||
type Error = Error;
|
||||
|
||||
async fn send_raw(&self, _envelope: &Envelope, _email: &[u8]) -> Result<Self::Ok, Self::Error> {
|
||||
self.response
|
||||
}
|
||||
}
|
||||
233
testdata/email_with_png.eml
vendored
Normal file
233
testdata/email_with_png.eml
vendored
Normal file
@@ -0,0 +1,233 @@
|
||||
Date: Tue, 15 Nov 1994 08:12:31 GMT
|
||||
From: NoBody <nobody@domain.tld>
|
||||
Reply-To: Yuin <yuin@domain.tld>
|
||||
To: Hei <hei@domain.tld>
|
||||
Subject: Happy new year
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/related; boundary="0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1"
|
||||
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: text/html; charset=utf8
|
||||
Content-Transfer-Encoding: 7bit
|
||||
|
||||
<p><b>Hello</b>, <i>world</i>! <img src=cid:123></p>
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1
|
||||
Content-Type: image/png
|
||||
Content-Disposition: inline
|
||||
Content-ID: <123>
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
iVBORw0KGgoAAAANSUhEUgAAA+gAAAPoCAYAAABNo9TkAAAACXBIWXMAASdGAAEnRgHWSSfaAAAA
|
||||
GXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAIABJREFUeJzs3WmMlfXd//HvzLAO
|
||||
izAORCmKgKmKKQa1taUWl1C1cWm1rVvdSls01bbGpcsDTWOrMcabdNNiXdqkrSamMbY2hkTbOiyD
|
||||
0HFBiRSLE6WFKgoFFIRBztwP7M2//gEVnJnf95zzeiUmLjDziYlnztvrd12noaOjozsAAACAohpL
|
||||
DwAAAAAEOgAAAKQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ9Cs9ADL6wx/+EHfffXfpGQB7ZNiw
|
||||
YfGb3/ym9AwAYC8JdNiFN954I1atWlV6BsAeGT58eOkJAMAH4Ig7AAAAJCDQAQAAIAGBDgAAAAkI
|
||||
dAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhA
|
||||
oAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEAC
|
||||
Ah0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAAS
|
||||
EOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQ
|
||||
gEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACA
|
||||
BAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAA
|
||||
JCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAA
|
||||
IAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAA
|
||||
AAkIdAAAAEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAA
|
||||
AEhAoAMAAEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMA
|
||||
AEACAh0AAAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0A
|
||||
AAASEOgAAACQgEAHAACABAQ6AAAAJCDQAQAAIAGBDgAAAAkIdAAAAEhAoAMAAEACAh0AAAASEOgA
|
||||
AACQgEAHAACABAQ6AAAAJCDQAQAAIIF+pQdARieeeGIcfPDBpWdQJ55//vn40Y9+VHoGAACFCXTY
|
||||
hf322y/222+/0jOoE01NTaUnAACQgCPuAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABA
|
||||
AgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAA
|
||||
EhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAA
|
||||
kIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAA
|
||||
gAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAA
|
||||
ACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEA
|
||||
ACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4A
|
||||
AAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQA
|
||||
AABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKAD
|
||||
AABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgId
|
||||
AAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDo
|
||||
AAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBA
|
||||
BwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQE
|
||||
OgAAACQg0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg
|
||||
0AEAACABgQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACAB
|
||||
gQ4AAAAJCHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJ
|
||||
CHQAAABIQKADAABAAgIdAAAAEhDoAAAAkIBABwAAgAQEOgAAACQg0AEAACABgQ4AAAAJCHQAAABI
|
||||
QKADFNbY6KUYAACBDlBcU1NT6QkAACQg0AEK69evX+kJAAAkINABCnMFHQCACIEOUJxABwAgQqAD
|
||||
FOeIOwAAEQIdoDhPcaenbN++vfQEAOAD8K4QoDBX0Okpb731VukJAMAHINABChPo9BSBDgDVTaAD
|
||||
FOYhcfSU7du3R3d3d+kZAMBeEugAhQl0epL70AGgegl0gML69+9fegI1pKurq/QEAGAvCXSAwgYP
|
||||
HhwNDQ2lZ1AjXn/99dITAIC9JNABCmtsbIyBAweWnkGNEOgAUL0EOkACzc3NpSdQIwQ6AFQvgQ6Q
|
||||
gECnpwh0AKheAh0ggSFDhpSeQI144403Sk8AAPaSQAdIwBV0esprr71WegIAsJcEOkACAp2esmbN
|
||||
mtITAIC9JNABEnDEnZ4i0AGgegl0gAQGDx5cegI1QqADQPUS6AAJuIJOTxHoAFC9BDpAAgKdnvLa
|
||||
a6/F1q1bS88AAPaCQAdIYPjw4aUnUCMqlUqsXLmy9AwAYC8IdIAEWlpaSk+ghrz44oulJwAAe0Gg
|
||||
AyQwcuTI0hOoIQIdAKqTQAdIQKDTkwQ6AFQngQ6QgCPu9KTly5eXngAA7AWBDpDAyJEjo7HRSzI9
|
||||
46WXXoo33nij9AwAYA95NwiQQGNjoye502O6u7tdRQeAKiTQAZJwzJ2e9Nxzz5WeAADsIYEOkMSI
|
||||
ESNKT6CGLF26tPQEAGAPCXSAJFxBpyd1dHREpVIpPQMA2AMCHSAJgU5P2rBhQ6xYsaL0DABgDwh0
|
||||
gCRaW1tLT6DGdHR0lJ4AAOwBgQ6QxJgxY0pPoMYsXry49AQAYA8IdIAk9t9//9ITqDEdHR3x5ptv
|
||||
lp4BALxPAh0gCYFOT9uyZUssWrSo9AwA4H0S6ABJtLa2Rv/+/UvPoMb85S9/KT0BAHifBDpAEo2N
|
||||
jTF69OjSM6gxc+fOjW3btpWeAQC8DwIdIBEPiqOnvf766x4WBwBVQqADJOI+dHrDQw89VHoCAPA+
|
||||
CHSARPbbb7/SE6hBbW1tsX79+tIzAID3INABEnEFnd6wbdu2mDNnTukZAMB7EOgAibgHnd7ywAMP
|
||||
RHd3d+kZAMC7EOgAiRx44IGlJ1CjOjs7Y+HChaVnAADvQqADJDJq1KgYOnRo6RnUqHvvvbf0BADg
|
||||
XQh0gGQOOuig0hOoUY8//ng8//zzpWcAALsh0AGSmTBhQukJ1LBf/epXpScAALsh0AGScQWd3vTI
|
||||
I4+4ig4ASQl0gGTGjx9fegI1rLu7O2bPnl16BgCwCwIdIBlH3Oltc+fOjWeffbb0DADg/yPQAZLZ
|
||||
f//9Y+DAgaVnUONuueWWqFQqpWcAAP9FoAMk09jYGOPGjSs9gxq3bNmyePDBB0vPAAD+i0AHSMiD
|
||||
4ugLP/vZz2L9+vWlZwAA/yHQARI6+OCDS0+gDmzcuDFmzZpVegYA8B8CHSChQw89tPQE6sTDDz8c
|
||||
jz76aOkZAEAIdICUJk2aVHoCdeTmm2+OtWvXlp4BAHVPoAMkNGLEiBgzZkzpGdSJ9evXxw033OCp
|
||||
7gBQmEAHSOqwww4rPYE6smDBgrjnnntKzwCAuibQAZIS6PS1X/ziF9He3l56BgDULYEOkJT70Olr
|
||||
lUolrrvuuli5cmXpKQBQlwQ6QFKTJk2KhoaG0jOoMxs2bIgrrrgi1q1bV3oKANQdgQ6Q1NChQ+OA
|
||||
Aw4oPYM6tHr16rjyyivjzTffLD0FAOqKQAdIzOehU8pzzz0X3/72t6Orq6v0FACoGwIdILHDDz+8
|
||||
9ATq2MKFC+Oaa64R6QDQRwQ6QGJTpkwpPYE6197eHtdee61IB4A+INABEjvkkENiyJAhpWdQ5xYs
|
||||
WBCXXnpprF+/vvQUAKhpAh0gsaampjjiiCNKz4B49tlnY+bMmfHyyy+XngIANUugAyR31FFHlZ4A
|
||||
ERHR2dkZM2bMiKVLl5aeAgA1SaADJHfkkUeWngA7rFmzJmbOnBkPPPBA6Skk0dXV5RkFAD1EoAMk
|
||||
d9hhh8XgwYNLz4Adurq64qabboof/OAHPiu9jj333HNxyy23xCmnnBJz584tPQegJgh0gOT69esX
|
||||
kydPLj0DdvL73/8+zjnnnFiyZEnpKfSRtWvXxn333Rfnn39+XHTRRXH//ffHxo0bBTpAD+lXegAA
|
||||
7+2oo46KRYsWlZ4BO1m9enVceuml8ZWvfCUuueSS6N+/f+lJ9LCurq6YP39+PPTQQ9He3h7bt2/f
|
||||
6dfMnz8/tm/fHk1NTQUWAtQOgQ5QBdyHTmZvvfVW3HHHHfHII4/E9773vZgyZUrpSfSAZcuWxR//
|
||||
+MeYM2dObNiw4V1/7caNG2PJkiVeqwA+IIEOUAUmTZoUgwYNii1btpSeArvV2dkZM2fOjNNPPz0u
|
||||
v/zy2HfffUtPYg8tW7Ys/vznP8ef/vSnWLly5R793ra2NoEO8AE1dHR0dJceAcB7u+qqq9znSdUY
|
||||
PHhwnH322TFjxowYMmRI6Tm8i87Oznj00Udjzpw5exzl/23s2LHx4IMP9uAygPoj0AGqxAMPPBA3
|
||||
3XRT6RmwR1pbW+Piiy+OM888MwYNGlR6DhFRqVTimWee2XGl/JVXXumxr33//ffHhAkTeuzrAdQb
|
||||
gQ5QJdasWROnnnpqdHd72ab6tLS0xAUXXBBf+MIXorm5ufScurN58+b461//GgsXLoy2trZ49dVX
|
||||
e+X7XHHFFXHJJZf0ytcGqAcCHaCKnH/++fH888+XngF7rbm5OU455ZQ499xzXWntRZVKJZYvXx6L
|
||||
Fy+ORYsWxVNPPRXbtm3r9e87efLkuOeee3r9+wDUKoEOUEVuv/12b36pCQ0NDXHMMcfE6aefHscf
|
||||
f3wMHDiw9KSq9+qrr8bjjz8eCxcujMWLF8f69ev7fENjY2PMmTMnWlpa+vx7A9QCgQ5QRZ555pmY
|
||||
MWNG6RnQo4YOHRrTp0+Pk08+OY466qhobGwsPSm97u7ueOmll+LZZ5+NZ555JpYsWRKdnZ2lZ0VE
|
||||
xHXXXRef/exnS88AqEoCHaCKVCqVOOmkk4pcGYO+MGLEiJg2bVqccMIJcfTRR8fgwYNLT0rhzTff
|
||||
jOeeey6WLFmyI8rf67PJS5k2bVrMmjWr9AyAqiTQAarM9ddfHw8//HDpGdDrBgwYEJMnT46Pf/zj
|
||||
cfTRR8ehhx4a/fr1Kz2r161bty5WrFgRnZ2d8cILL8SyZcvi73//e2zfvr30tPdl0KBB8eijj3pq
|
||||
P8BeqP2fcgA15thjjxXo1IWurq7o6OiIjo6OiHg7/CZNmhRHHHFEHHLIITFx4sQ44IADqjbaN27c
|
||||
GC+++GKsWLEiXnjhhejs7IwVK1bEv//979LTPpAtW7bE4sWLY9q0aaWnAFSd6vyJBlDHpk6dGgMG
|
||||
DIiurq7SU6BPbdmyJZ588sl48sknd/y9/v37x7hx42LChAkxceLEGD9+fIwfPz5Gjx4dQ4YMKba1
|
||||
UqnE2rVr41//+le8/PLL8fLLL+/489WrV8fLL78cmzZtKravt7W1tQl0gL0g0AGqzNChQ2Pq1Knx
|
||||
2GOPlZ4CxW3bti1WrFgRK1as2OmfDRw4MFpbW6O1tTX23XffaG1tjZaWlmhpaYmmpqYYMmRINDQ0
|
||||
xLBhwyLi7f+2Ghoa3vG1t2zZEhERW7duja1bt0bE2/eDb9q0KTZu3Ljjjw0bNsTrr78eGzZs2PHX
|
||||
lUqlD/4N5DRv3ryoVCoe+AewhwQ6QBU66aSTBDq8h61bt8aqVati1apVpafUnXXr1sXSpUtj8uTJ
|
||||
pacAVBX/WxOgCk2bNi2am5tLzwDYrXnz5pWeAFB1BDpAFRo0aFAce+yxpWcA7FZbW1vpCQBVR6AD
|
||||
VKmTTjqp9ASA3ers7IyVK1eWngFQVQQ6QJWaOnXqjodbAWTkmDvAnhHoAFVqwIABcfzxx5eeAbBb
|
||||
c+fOLT0BoKoIdIAq9ulPf7r0BIDdeuqpp2LDhg2lZwBUDYEOUMU+9rGPRUtLS+kZALtUqVRiwYIF
|
||||
pWcAVA2BDlDF+vXrF6eddlrpGQC75Zg7wPsn0AGq3FlnnRUNDQ2lZwDsUnt7e3R1dZWeAVAVBDpA
|
||||
lRs7dmwcddRRpWcA7NLmzZvjiSeeKD0DoCoIdIAacOaZZ5aeALBbPm4N4P0R6AA14MQTT4yRI0eW
|
||||
ngGwS4899lh0d3eXngGQnkAHqAH9+/ePU089tfQMgF1as2ZNLF++vPQMgPQEOkCNOPPMMz0sDkjL
|
||||
09wB3ptAB6gR48aNiylTppSeAbBLAh3gvQl0gBry+c9/vvQEgF1avnx5vPLKK6VnAKQm0AFqyPTp
|
||||
02P//fcvPQNgJ93d3a6iA7wHgQ5QQ5qamuK8884rPQNglwQ6wLsT6AA15nOf+1wMGzas9AyAnXR0
|
||||
dMSmTZtKzwBIS6AD1Jjm5uY466yzSs8A2Mm2bdti4cKFpWcApCXQAWrQl770pRgwYEDpGQA7ccwd
|
||||
YPcEOkANamlpiVNOOaX0DICdzJ8/P7Zv3156BkBKAh2gRl100UXR2OhlHshl48aN8fTTT5eeAZCS
|
||||
d24ANeqggw6KT3ziE6VnAOzEMXeAXRPoADVsxowZpScA7KStra30BICUBDpADTviiCNi6tSppWcA
|
||||
vMM///nP6OzsLD0DIB2BDlDjvv71r0dDQ0PpGQDv4Co6wM4EOkCNO/TQQ+OEE04oPQPgHebNm1d6
|
||||
AkA6Ah2gDlx++eXR1NRUegbADkuXLo21a9eWngGQikAHqAPjxo2Lk08+ufQMgB0qlUrMnz+/9AyA
|
||||
VAQ6QJ247LLLon///qVnAOzgPnSAdxLoAHVizJgxcfrpp5eeAbDDokWLYsuWLaVnAKQh0AHqyFe/
|
||||
+tUYPHhw6RkAERGxdevWWLx4cekZAGkIdIA6Mnr06Pjyl79cegbADo65A/w/Ah2gzlx44YVx4IEH
|
||||
lp4BEBFvf9xapVIpPQMgBYEOUGf69+8f3/rWt0rPAIiIiHXr1sXSpUtLzwBIQaAD1KHjjjsupk6d
|
||||
WnoGQES8fRUdAIEOULeuvfbaGDBgQOkZAO5DB/gPgQ5Qpw444IA455xzSs8AiM7Ozli5cmXpGQDF
|
||||
CXSAOva1r30tRo0aVXoGgGPuACHQAepac3NzfPOb3yw9AyDmzp1begJAcQIdoM595jOfiRNOOKH0
|
||||
DKDOPfXUU7Fhw4bSMwCKEugAxHe+850YPnwtwJqJAAAO10lEQVR46RlAHatUKrFgwYLSMwCKEugA
|
||||
RGtra1x11VWlZwB1zjF3oN4JdAAiIuK0006L4447rvQMoI61t7dHV1dX6RkAxQh0AHb47ne/66g7
|
||||
UMzmzZvjiSeeKD0DoBiBDsAOo0aNiiuvvLL0DKCOOeYO1DOBDsA7nHHGGTF16tTSM4A61dbWFt3d
|
||||
3aVnABQh0AHYyfe///3Yd999S88A6tCaNWti+fLlpWcAFCHQAdhJS0tL/PCHP4zGRj8mgL7nmDtQ
|
||||
r7zzAmCXPvrRj8bFF19cegZQhwQ6UK8EOgC7ddlll8WUKVNKzwDqzN/+9rdYvXp16RkAfU6gA7Bb
|
||||
TU1NceONN8aIESNKTwHqzPz580tPAOhzAh2AdzV69Oi44YYboqGhofQUoI7Mmzev9ASAPifQAXhP
|
||||
U6dOjQsuuKD0DKCOdHR0xKZNm0rPAOhTAh2A9+Xyyy+PI488svQMoE5s27YtFi5cWHoGQJ8S6AC8
|
||||
L/369Ytbb701DjzwwNJTgDrhae5AvRHoALxvw4cPj1mzZsWwYcNKTwHqwPz582P79u2lZwD0GYEO
|
||||
wB456KCD4uabb46mpqbSU4Aat3Hjxnj66adLzwDoMwIdgD12zDHHxNVXX116BlAH2traSk8A6DMC
|
||||
HYC9cvbZZ8cXv/jF0jOAGifQgXoi0AHYa9dee2188pOfLD0DqGGrVq2Kzs7O0jMA+oRAB2CvNTY2
|
||||
xo033hgf/vCHS08Bapir6EC9EOgAfCBDhw6N2267LcaPH196ClCj5s2bV3oCQJ8Q6AB8YCNHjozb
|
||||
brstxowZU3oKUIOWLl0aa9euLT0DoNcJdAB6xOjRo+P222+P1tbW0lOAGlOpVFxFB+qCQAegx4wd
|
||||
OzZuu+222GeffUpPAWrM3LlzS08A6HUCHYAeNXHixPjJT34Szc3NpacANWTRokWxZcuW0jMAepVA
|
||||
B6DHHX744TFr1qwYNGhQ6SlAjdi6dWssXry49AyAXiXQAegVRx99dPz0pz+NIUOGlJ4C1AgftwbU
|
||||
OoEOQK+ZMmVKzJ49O0aMGFF6ClAD5s2bF5VKpfQMgF4j0AHoVYcddljceeedMXr06NJTgCq3bt26
|
||||
WLp0aekZAL1GoAPQ68aPHx933XVXjB07tvQUoMr5uDWglgl0APrEmDFj4q677oqJEyeWngJUMfeh
|
||||
A7VMoAPQZ1pbW+PnP/95HHLIIaWnAFWqs7Mz/vGPf5SeAdArBDoAfaqlpSXuvPPOOO6440pPAarU
|
||||
3LlzS08A6BUCHYA+19zcHLfeemvMnDmz9BSgCgl0oFYJdACKaGhoiJkzZ8b1118f/fv3Lz0HqCJP
|
||||
PfVUrF+/vvQMgB4n0AEo6owzzojZs2fHyJEjS08BqkSlUon29vbSMwB6nEAHoLgjjjgifvnLX8b4
|
||||
8eNLTwGqhGPuQC0S6ACkMHbs2Ljnnnti6tSppacAVaC9vT26urpKzwDoUQIdgDSGDRsWP/7xj+Pq
|
||||
q6+Ofv36lZ4DJLZ58+Z44oknSs8A6FECHYBUGhoa4rzzzou77747xowZU3oOkJhj7kCtEegApHT4
|
||||
4YfHr3/96/jUpz5VegqQVFtbW3R3d5eeAdBjBDoAae2zzz4xa9asuPrqq30UG7CTNWvWxPLly0vP
|
||||
AOgxAh2A1P77yPuHPvSh0nOAZBxzB2qJQAegKkyaNCnuvffeOPfcc6Ox0Y8v4G1tbW2lJwD0GO9w
|
||||
AKgaQ4YMiWuuuSbuuOOOOPDAA0vPARJYvnx5rF69uvQMgB4h0AGoOlOmTIn77rsvLr74YlfTgZg/
|
||||
f37pCQA9wrsaAKrSwIED4xvf+EbcddddcdBBB5WeAxQ0b9680hMAeoRAB6CqTZ48OX7729/GhRde
|
||||
6Go61KmOjo7YvHlz6RkAH1hDR0eHD48EoCa8+OKLMWvWrGhvby89BegDEyZMiOnTp8f06dNjwoQJ
|
||||
pecAfGACHYCaM3fu3Pif//mfWLVqVekpQA9qbGyMj3zkIzFt2rQ44YQTPCwSqDkCHYCatG3btvjd
|
||||
734Xs2fPjk2bNpWeA+yl/4vy/7tSPmrUqNKTAHqNQAegpr322mvxi1/8b3t391J1usZx+M6VQrqW
|
||||
tbSyLEgtCUtIijRNI6K/sKPaMOcdVVBBUVTUQVRSIpLlQfRGlNILaEYWug82IxMje2aY6nfbui74
|
||||
4WKh+D3TDz74/CfOnTsXi4uLRc8B/oaGhoYYGBiIkZGROHr0aLS0tBQ9CeCnEOgA1ISJiYk4ceJE
|
||||
3L9/v+gpwAqamppieHg4jh07FkNDQ7Fu3bqiJwH8dAIdgJoyNjYWp06dinv37hU9BWpec3NzDA8P
|
||||
x/Hjx+PQoUPR0NBQ9CSAQgl0AGrS2NhYnDx5MkZHR4ueAjVl27ZtMTIyEkeOHIn9+/fH2rVri54E
|
||||
kIZAB6CmCXX4serq6mL37t0xMjLiOjSAvyDQASAibt++Hb/99luMjY0VPQVWvXK5HENDQ3HkyJEY
|
||||
GhqK5ubmoicBrAoCHQD+4NGjR3HmzJm4cOFCLCwsFD0HVo329vY4dOhQDA8Px+DgYNTX1xc9CWDV
|
||||
EegAsIJ3797F+fPn4/Tp0zEzM1P0HEjnj0fXR0ZGoqenp+hJAKueQAeA/2NhYSEuXboUp0+fjqmp
|
||||
qaLnQKE2bNgQ/f39MTQ0FIcPH45qtVr0JIBfikAHgL9pdHQ0zpw5Ezdu3HD8nZpQKpWit7c3BgcH
|
||||
Y3BwMHp6eqKurq7oWQC/LIEOAP/Q7OxsXLlyJS5evBjj4+OxtORHKb+O9vb2GBgYiMHBwTh48GBU
|
||||
KpWiJwHUDIEOAP/C69ev4/Lly3H27Nl4+fJl0XPgH6tWq3HgwIHo7++Pvr4+16ABFEigA8B3sLi4
|
||||
GA8ePIgLFy7E9evXY25uruhJsKJyuRz79++PgwcPRn9/f3R1dcWaNWuKngVACHQA+O4WFxdjfHw8
|
||||
rl69GteuXYvp6emiJ1HDWlpaYu/evdHX1xf79u2L3t7eWLt2bdGzAFiBQAeAH+zJkydx9erVuHXr
|
||||
VkxOThY9h1/cxo0bo6+vb/nIemdnp7+QA6wSAh0AfqLnz5/HjRs34ubNmzExMRFfv34tehKrWKVS
|
||||
ib1790Zvb+/yR1efAaxeAh0ACjI/Px9jY2MxOjoao6Oj8ejRo1hcXCx6FknV19fHrl27vonxHTt2
|
||||
+Os4wC9EoANAEvPz8zExMRF37tyJu3fvxuPHjwV7jSqXy7Fz587o6emJrq6u6Orqij179kRDQ0PR
|
||||
0wD4gQQ6ACT1/v37GB8fj4cPH8bk5GQ8fPgwPnz4UPQsvqOGhobo6OiIjo6O2LlzZ3R3d0d3d3ds
|
||||
3bq16GkAFECgA8Aq8urVq+VYn5ycjMnJyZidnS16Fn+hUqnEjh07oqurKzo6OqKzszM6Ozujvb09
|
||||
6urqip4HQBICHQBWsaWlpXjx4kVMTU3F8+fP4+nTp/H06dN49uxZfPz4seh5NaNUKkVbW1ts3749
|
||||
tm3b9s2zffv2aG5uLnoiAKuAQAeAX9TMzEw8e/Zs+fk93Kenp+PLly9Fz1s1SqVStLa2xpYtW2Lj
|
||||
xo3R1tYWmzdvjk2bNi2/bmtrc7c4AP+aQAeAGvT27duYmZmJ6enpmJ6ejpmZmXjz5s03r+fn54ue
|
||||
+UPU19dHpVKJDRs2LD/VajWq1WqsX79++b3W1taoVqvR2trqGDoAP4VABwBWND8/H7Ozs988c3Nz
|
||||
MTc3t+L7S0tLMTc3t/z1s7OzsbT0v18zPn36tHzn++fPn+PLly9RLpdX/L7lcvlPV4f9fkS8Uqks
|
||||
f05dXV00NTVFqVSKxsbGKJVK0dTU9KenUqlEuVyOpqamKJfL/hM6AGk5iwUArKixsTEaGxujra2t
|
||||
6CkAUBOc1wIAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCA
|
||||
QAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAE
|
||||
BDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAk
|
||||
INABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAg
|
||||
AYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAA
|
||||
CQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAA
|
||||
SECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAA
|
||||
QAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAA
|
||||
ABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAA
|
||||
AJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcA
|
||||
AIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoA
|
||||
AAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINAB
|
||||
AAAgAYEOAAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEO
|
||||
AAAACQh0AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0
|
||||
AAAASECgAwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECg
|
||||
AwAAQAICHQAAABIQ6AAAAJCAQAcAAIAEBDoAAAAkINABAAAgAYEOAAAACQh0AAAASECgAwAAQAIC
|
||||
HQAAABIQ6AAAAJDAfwHNjj3TR6+CggAAAABJRU5ErkJggg==
|
||||
--0oVZ2r6AoLAhLlb0gPNSKy6BEqdS2IfwxrcbUuo1--
|
||||
14
tests/lib.rs
14
tests/lib.rs
@@ -1,14 +0,0 @@
|
||||
// Copyright 2014 Alexis Mousset. See the COPYRIGHT
|
||||
// file at the top-level directory of this distribution.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
|
||||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
|
||||
// option. This file may not be copied, modified, or distributed
|
||||
// except according to those terms.
|
||||
|
||||
#[test]
|
||||
|
||||
fn foo() {
|
||||
assert!(true);
|
||||
}
|
||||
166
tests/transport_file.rs
Normal file
166
tests/transport_file.rs
Normal file
@@ -0,0 +1,166 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "file-transport", feature = "builder"))]
|
||||
mod test {
|
||||
use lettre::{transport::file::FileTransport, Message};
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{read_to_string, remove_file},
|
||||
};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "file-transport-envelope")]
|
||||
fn file_transport_with_envelope() {
|
||||
use lettre::Transport;
|
||||
let sender = FileTransport::with_envelope(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
let json_file = temp_dir().join(format!("{}.json", id));
|
||||
let json = read_to_string(&json_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
json,
|
||||
"{\"forward_path\":[\"hei@domain.tld\"],\"reverse_path\":\"nobody@domain.tld\"}"
|
||||
);
|
||||
remove_file(json_file).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn file_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn file_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender = FileTransport::new(temp_dir());
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
let id = result.unwrap();
|
||||
|
||||
let eml_file = temp_dir().join(format!("{}.eml", id));
|
||||
let eml = read_to_string(&eml_file).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
eml,
|
||||
concat!(
|
||||
"From: NoBody <nobody@domain.tld>\r\n",
|
||||
"Reply-To: Yuin <yuin@domain.tld>\r\n",
|
||||
"To: Hei <hei@domain.tld>\r\n",
|
||||
"Subject: Happy new year\r\n",
|
||||
"Date: Tue, 15 Nov 1994 08:12:31 GMT\r\n",
|
||||
"Content-Transfer-Encoding: 7bit\r\n",
|
||||
"\r\n",
|
||||
"Be happy!"
|
||||
)
|
||||
);
|
||||
remove_file(eml_file).unwrap();
|
||||
}
|
||||
}
|
||||
65
tests/transport_sendmail.rs
Normal file
65
tests/transport_sendmail.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "sendmail-transport", feature = "builder"))]
|
||||
mod test {
|
||||
use lettre::{transport::sendmail::SendmailTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport() {
|
||||
use lettre::Transport;
|
||||
let sender = SendmailTransport::new();
|
||||
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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(&email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn sendmail_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender = SendmailTransport::new();
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn sendmail_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender = SendmailTransport::new();
|
||||
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")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
let result = sender.send(email).await;
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
21
tests/transport_smtp.rs
Normal file
21
tests/transport_smtp.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(all(feature = "smtp-transport", feature = "builder"))]
|
||||
mod test {
|
||||
use lettre::{Message, SmtpTransport, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
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")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build()
|
||||
.send(&email)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
60
tests/transport_smtp_pool.rs
Normal file
60
tests/transport_smtp_pool.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "r2d2"))]
|
||||
mod test {
|
||||
use lettre::address::Envelope;
|
||||
use lettre::{SmtpTransport, Transport};
|
||||
|
||||
use std::{sync::mpsc, thread};
|
||||
|
||||
fn envelope() -> Envelope {
|
||||
Envelope::new(
|
||||
Some("user@localhost".parse().unwrap()),
|
||||
vec!["root@localhost".parse().unwrap()],
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_one() {
|
||||
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
let result = mailer.send_raw(&envelope(), b"test");
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_from_thread() {
|
||||
let mailer = SmtpTransport::builder_dangerous("127.0.0.1")
|
||||
.port(2525)
|
||||
.build();
|
||||
|
||||
let (s1, r1) = mpsc::channel();
|
||||
let (s2, r2) = mpsc::channel();
|
||||
|
||||
let mailer1 = mailer.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
s1.send(()).unwrap();
|
||||
r2.recv().unwrap();
|
||||
mailer1
|
||||
.send_raw(&envelope(), b"test1")
|
||||
.expect("Send failed from thread 1");
|
||||
});
|
||||
|
||||
let mailer2 = mailer.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
s2.send(()).unwrap();
|
||||
r1.recv().unwrap();
|
||||
mailer2
|
||||
.send_raw(&envelope(), b"test2")
|
||||
.expect("Send failed from thread 2");
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
mailer
|
||||
.send_raw(&envelope(), b"test")
|
||||
.expect("Send failed from main thread");
|
||||
}
|
||||
}
|
||||
65
tests/transport_stub.rs
Normal file
65
tests/transport_stub.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "builder")]
|
||||
mod test {
|
||||
use lettre::{transport::stub::StubTransport, Message};
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
use tokio02_crate as tokio;
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
use lettre::Transport;
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(&email).unwrap();
|
||||
sender_ko.send(&email).unwrap_err();
|
||||
}
|
||||
|
||||
#[cfg(feature = "async-std1")]
|
||||
#[async_std::test]
|
||||
async fn stub_transport_asyncstd1() {
|
||||
use lettre::AsyncStd1Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
|
||||
#[cfg(feature = "tokio02")]
|
||||
#[tokio::test]
|
||||
async fn stub_transport_tokio02() {
|
||||
use lettre::Tokio02Transport;
|
||||
|
||||
let sender_ok = StubTransport::new_ok();
|
||||
let sender_ko = StubTransport::new_error();
|
||||
let email = Message::builder()
|
||||
.from("NoBody <nobody@domain.tld>".parse().unwrap())
|
||||
.reply_to("Yuin <yuin@domain.tld>".parse().unwrap())
|
||||
.to("Hei <hei@domain.tld>".parse().unwrap())
|
||||
.subject("Happy new year")
|
||||
.date("Tue, 15 Nov 1994 08:12:31 GMT".parse().unwrap())
|
||||
.body(String::from("Be happy!"))
|
||||
.unwrap();
|
||||
|
||||
sender_ok.send(email.clone()).await.unwrap();
|
||||
sender_ko.send(email).await.unwrap_err();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user