Compare commits
562 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75ab05229a | ||
|
|
393ef8dcd1 | ||
|
|
ceb57edfdd | ||
|
|
cf8f934c56 | ||
|
|
df949f837e | ||
|
|
0a3d51dc25 | ||
|
|
4828cf4e92 | ||
|
|
c33de49fbb | ||
|
|
4f470a2c3f | ||
|
|
a0c8fb947c | ||
|
|
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 | ||
|
|
5f911dce12 | ||
|
|
47d6870d93 | ||
|
|
51de392086 | ||
|
|
fefb5f7978 | ||
|
|
5d125bdbdb | ||
|
|
a1bf0170db | ||
|
|
5bedba4b24 | ||
|
|
813f09a314 | ||
|
|
6a6023431b | ||
|
|
7f6aa0ffae | ||
|
|
1830f084c0 | ||
|
|
49a995f68d | ||
|
|
2fd5147a0f | ||
|
|
9c34b5a055 | ||
|
|
8d60068831 | ||
|
|
c2506b47fc | ||
|
|
cecc11f74e | ||
|
|
5ecdabbce7 | ||
|
|
bd4680d793 | ||
|
|
56f93bd614 | ||
|
|
c9a657ac44 | ||
|
|
df8c8a18a8 | ||
|
|
e30e96c1ca | ||
|
|
ef8c426cd4 | ||
|
|
0b7e004ac8 | ||
|
|
599cf6c313 | ||
|
|
e6b46faa06 | ||
|
|
75de338409 | ||
|
|
29c9b7661e | ||
|
|
d46bbeebf0 | ||
|
|
ae3fc78e67 | ||
|
|
26dae6ecbd | ||
|
|
e855e37182 | ||
|
|
3195959de8 | ||
|
|
5b70dccfd3 | ||
|
|
3b78455b22 | ||
|
|
cbc9764626 | ||
|
|
8811b32113 | ||
|
|
0b185352f8 | ||
|
|
72390e9bdb | ||
|
|
520db5d21d | ||
|
|
bb0c9c5c5c | ||
|
|
262f3b8d93 | ||
|
|
d1b6b33b44 | ||
|
|
4f249b319c | ||
|
|
2281b9046e | ||
|
|
a4ada1b6ef | ||
|
|
8da3a09140 | ||
|
|
6ea49d770c | ||
|
|
6a5b100098 | ||
|
|
5ed452919c | ||
|
|
30583ef1a6 | ||
|
|
60356c3b52 | ||
|
|
ed06c67258 | ||
|
|
ee8da8196a | ||
|
|
78e88d5bc6 | ||
|
|
8f49ba3b17 | ||
|
|
6d19f80842 | ||
|
|
02a9093bad | ||
|
|
d59672ef26 | ||
|
|
7f9b2c5150 |
14
.appveyor.yml
Normal file
14
.appveyor.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
environment:
|
||||
matrix:
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
install:
|
||||
- curl -sSf -o rustup-init.exe https://win.rustup.rs/
|
||||
- rustup-init.exe -y --default-host %TARGET%
|
||||
- set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
|
||||
- rustc -vV
|
||||
- cargo -vV
|
||||
build: false
|
||||
test_script:
|
||||
- cargo build --verbose --manifest-path lettre/Cargo.toml
|
||||
- cargo test --verbose --manifest-path lettre_email/Cargo.toml
|
||||
|
||||
20
.build-scripts/codecov.sh
Executable file
20
.build-scripts/codecov.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
wget https://github.com/SimonKagstrom/kcov/archive/master.tar.gz
|
||||
tar xzf master.tar.gz
|
||||
cd kcov-master
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
make
|
||||
make install DESTDIR=../../kcov-build
|
||||
cd ../..
|
||||
rm -rf kcov-master
|
||||
for file in target/debug/lettre*[^\.d]; do
|
||||
mkdir -p "target/cov/$(basename $file)"
|
||||
./kcov-build/usr/local/bin/kcov --exclude-pattern=/.cargo,/usr/lib --verify "target/cov/$(basename $file)" "$file"
|
||||
done
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
echo "Uploaded code coverage"
|
||||
10
.build-scripts/site-upload.sh
Executable file
10
.build-scripts/site-upload.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -xe
|
||||
|
||||
cd website
|
||||
make clean && make
|
||||
echo "lettre.at" > _book/html/CNAME
|
||||
sudo pip install ghp-import
|
||||
ghp-import -n _book/html
|
||||
git push -f https://${GH_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
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"]
|
||||
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.
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,2 +1,6 @@
|
||||
/target/
|
||||
.vscode/
|
||||
.project/
|
||||
.idea/
|
||||
lettre.iml
|
||||
target/
|
||||
/Cargo.lock
|
||||
|
||||
41
.travis.yml
41
.travis.yml
@@ -1,14 +1,29 @@
|
||||
language: rust
|
||||
|
||||
after_success: |
|
||||
[ $TRAVIS_BRANCH = master ] &&
|
||||
[ $TRAVIS_PULL_REQUEST = false ] &&
|
||||
cargo doc &&
|
||||
echo '<meta http-equiv=refresh content=0;url=smtp/index.html>' > target/doc/index.html &&
|
||||
sudo pip install ghp-import &&
|
||||
ghp-import -n target/doc &&
|
||||
git push -fq https://${TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git gh-pages
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: m8BXCWDfBh0Hkajp7+KdSFP65q3gUuJWJSb+W6BtAzWNxIuAwTgtm3Ja3gwwlko+XSOO4IG8+p+ApnduK1znWO+mslhUC7qr8oxIWRS3CzvSr3YeEwfVbWOz34/+I5cGh91ie8pbqxP+buMPVj078W3Cr/STzHhS2ZdQpjR/L8c=
|
||||
rust:
|
||||
- stable
|
||||
- beta
|
||||
- nightly
|
||||
- 1.32.0
|
||||
matrix:
|
||||
allow_failures:
|
||||
- rust: nightly
|
||||
sudo: required
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- postfix
|
||||
- libcurl4-openssl-dev
|
||||
- libelf-dev
|
||||
- libdw-dev
|
||||
- cmake
|
||||
- gcc
|
||||
- binutils-dev
|
||||
- libiberty-dev
|
||||
before_script:
|
||||
- smtp-sink 2525 1000&
|
||||
- sudo chgrp -R postdrop /var/spool/postfix/maildrop
|
||||
script:
|
||||
- cargo test --verbose --all --all-features
|
||||
after_success:
|
||||
- ./.build-scripts/codecov.sh
|
||||
- '[ "$TRAVIS_RUST_VERSION" = "stable" ] && [ "$TRAVIS_BRANCH" = "v0.9.x" ] && [ $TRAVIS_PULL_REQUEST = false ] && ./.build-scripts/site-upload.sh'
|
||||
|
||||
170
CHANGELOG.md
Normal file
170
CHANGELOG.md
Normal file
@@ -0,0 +1,170 @@
|
||||
<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.at. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
35
CONTRIBUTING.md
Normal file
35
CONTRIBUTING.md
Normal file
@@ -0,0 +1,35 @@
|
||||
## 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.
|
||||
@@ -1,7 +0,0 @@
|
||||
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. All files in the project carrying such
|
||||
notice may not be copied, modified, or distributed except
|
||||
according to those terms.
|
||||
25
Cargo.toml
25
Cargo.toml
@@ -1,20 +1,5 @@
|
||||
[package]
|
||||
|
||||
name = "smtp"
|
||||
version = "0.0.9"
|
||||
description = "Simple SMTP client"
|
||||
readme = "README.md"
|
||||
documentation = "http://amousset.github.io/rust-smtp/smtp/"
|
||||
repository = "https://github.com/amousset/rust-smtp"
|
||||
homepage = "https://github.com/amousset/rust-smtp"
|
||||
license = "MIT/Apache-2.0"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
|
||||
[dependencies]
|
||||
time = "*"
|
||||
uuid = "*"
|
||||
log = "*"
|
||||
env_logger = "*"
|
||||
rustc-serialize = "*"
|
||||
rust-crypto = "*"
|
||||
[workspace]
|
||||
members = [
|
||||
"lettre",
|
||||
"lettre_email",
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2014 Alexis Mousset
|
||||
Copyright (c) 2014-2018 Alexis Mousset
|
||||
|
||||
Permission is hereby granted, free of charge, to any
|
||||
person obtaining a copy of this software and associated
|
||||
201
LICENSE-APACHE
201
LICENSE-APACHE
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
107
README.md
107
README.md
@@ -1,55 +1,94 @@
|
||||
rust-smtp [](https://travis-ci.org/amousset/rust-smtp)
|
||||
=========
|
||||
# lettre
|
||||
|
||||
This library implements a simple SMTP client.
|
||||
See the [documentation](http://amousset.github.io/rust-smtp/smtp/) for more information.
|
||||
**Lettre is a mailer library for Rust.**
|
||||
|
||||
Rust versions
|
||||
-------------
|
||||
[](https://travis-ci.org/lettre/lettre)
|
||||
[](https://ci.appveyor.com/project/amousset/lettre/branch/master)
|
||||
[](https://codecov.io/gh/lettre/lettre)
|
||||
|
||||
This library is designed for Rust 1.0.0-nightly (master).
|
||||
[](https://crates.io/crates/lettre)
|
||||
[](https://docs.rs/lettre/)
|
||||
[]()
|
||||
[](./LICENSE)
|
||||
|
||||
Install
|
||||
-------
|
||||
[](https://gitter.im/lettre/lettre?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Average time to resolve an issue")
|
||||
[](http://isitmaintained.com/project/lettre/lettre "Percentage of issues still open")
|
||||
|
||||
Useful links:
|
||||
|
||||
* [User documentation](http://lettre.at/)
|
||||
* [API documentation](https://docs.rs/lettre/)
|
||||
* [Changelog](https://github.com/lettre/lettre/blob/master/CHANGELOG.md)
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
## Example
|
||||
|
||||
This library requires Rust 1.32 or newer.
|
||||
To use this library, add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
smtp = "*"
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
```
|
||||
|
||||
Otherwise, you can clone this repository and run `cargo build`.
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
|
||||
Example
|
||||
-------
|
||||
use lettre::{EmailTransport, SmtpTransport};
|
||||
use lettre_email::EmailBuilder;
|
||||
use std::path::Path;
|
||||
|
||||
There is an example command-line program included:
|
||||
```sh
|
||||
$ cargo test
|
||||
$ env RUST_LOG=info cargo run --example client -- -s "My subject" -r sender@localhost recipient@localhost < email.txt
|
||||
INFO:smtp::sender: connection established to 127.0.0.1:25
|
||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: from=<sender@localhost>
|
||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: to=<recipient@localhost>
|
||||
INFO:smtp::sender: 1d0467fb21b2454f90a85dd1e0eda839: conn_use=1, size=1889, status=sent (2.0.0 Ok: queued as BAA9C1C0055)
|
||||
INFO:client: Email sent successfully
|
||||
fn main() {
|
||||
let email = EmailBuilder::new()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpTransport::builder_unencrypted_localhost().unwrap()
|
||||
.build();
|
||||
// Send the email
|
||||
let result = mailer.send(&email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
Run `cargo run --example client -- -h` to get a list of available options.
|
||||
## Testing
|
||||
|
||||
Tests
|
||||
-----
|
||||
The `lettre` tests require an open mail server listening locally on port 2525 and the `sendmail` command.
|
||||
|
||||
You can build and run the tests with `cargo test`.
|
||||
## Code of conduct
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
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).
|
||||
|
||||
You can build the documentation with `cargo doc`. It is also available on [GitHub pages](http://amousset.github.io/rust-smtp/smtp/).
|
||||
## License
|
||||
|
||||
License
|
||||
-------
|
||||
This program is distributed under the terms of the MIT license.
|
||||
|
||||
This program is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
|
||||
|
||||
See LICENSE-APACHE, LICENSE-MIT, and COPYRIGHT for details.
|
||||
See [LICENSE](./LICENSE) for details.
|
||||
|
||||
@@ -1,156 +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.
|
||||
|
||||
#![feature(core, old_io, rustc_private, collections)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
extern crate env_logger;
|
||||
extern crate smtp;
|
||||
extern crate getopts;
|
||||
|
||||
use std::old_io::stdin;
|
||||
use std::string::String;
|
||||
use std::env;
|
||||
use getopts::{optopt, optflag, getopts, OptGroup, usage};
|
||||
use std::net::TcpStream;
|
||||
|
||||
use smtp::sender::{Sender, SenderBuilder};
|
||||
use smtp::error::SmtpResult;
|
||||
use smtp::mailer::EmailBuilder;
|
||||
|
||||
fn sendmail(source_address: String, recipient_addresses: Vec<String>, message: String, subject: String,
|
||||
server: String, port: u16, my_hostname: String, number: u16) -> SmtpResult {
|
||||
|
||||
let mut email_builder = EmailBuilder::new();
|
||||
for destination in recipient_addresses.iter() {
|
||||
email_builder = email_builder.to(destination.as_slice());
|
||||
}
|
||||
let email = email_builder.from(source_address.as_slice())
|
||||
.body(message.as_slice())
|
||||
.subject(subject.as_slice())
|
||||
.build();
|
||||
|
||||
let mut sender: Sender<TcpStream> = SenderBuilder::new((server.as_slice(), port)).hello_name(my_hostname.as_slice())
|
||||
.enable_connection_reuse(true).build();
|
||||
|
||||
for _ in (1..number) {
|
||||
let _ = sender.send(email.clone());
|
||||
}
|
||||
let result = sender.send(email);
|
||||
sender.close();
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn print_usage(description: String, _opts: &[OptGroup]) {
|
||||
println!("{}", usage(description.as_slice(), _opts));
|
||||
}
|
||||
|
||||
fn main() {
|
||||
env_logger::init().unwrap();
|
||||
|
||||
let args = env::args();
|
||||
|
||||
let mut args_string = Vec::new();
|
||||
for arg in args {
|
||||
args_string.push(arg.clone());
|
||||
};
|
||||
|
||||
let program = args_string[0].clone();
|
||||
let description = format!("Usage: {0} [options...] recipients\n\n\
|
||||
This program reads a message on standard input until it reaches\
|
||||
EOF, then tries to send it using the given paramters.\n\n\
|
||||
Example: {0} -r user@example.org user@example.com < message.txt",
|
||||
program);
|
||||
|
||||
let opts = [
|
||||
optopt("n", "number", "set the number of emails to send", "NUMBER"),
|
||||
optopt("s", "subject", "set the email subject", "SUBJECT"),
|
||||
optopt("r", "reverse-path", "set the sender address", "FROM_ADDRESS"),
|
||||
optopt("p", "port", "set the port to use, default is 25", "PORT"),
|
||||
optopt("a", "server", "set the server to use, default is localhost", "SERVER"),
|
||||
optopt("m", "my-hostname", "set the hostname used by the client", "MY_HOSTNAME"),
|
||||
optflag("h", "help", "print this help menu"),
|
||||
];
|
||||
|
||||
let matches = match getopts(args_string.tail(), &opts) {
|
||||
Ok(m) => m,
|
||||
Err(f) => panic!("{}", f),
|
||||
};
|
||||
|
||||
if matches.opt_present("h") {
|
||||
print_usage(description, &opts);
|
||||
return;
|
||||
}
|
||||
|
||||
if !matches.opt_present("r") {
|
||||
println!("The sender option is required");
|
||||
print_usage(program, &opts);
|
||||
return;
|
||||
}
|
||||
|
||||
let recipients_str: &str = if !matches.free.is_empty() {
|
||||
matches.free[0].as_slice()
|
||||
} else {
|
||||
print_usage(description, &opts);
|
||||
return;
|
||||
};
|
||||
|
||||
let mut recipients = Vec::new();
|
||||
for recipient in recipients_str.split(' ') {
|
||||
recipients.push(recipient.to_string());
|
||||
}
|
||||
|
||||
let mut message = String::new();
|
||||
|
||||
let mut line = stdin().read_line();
|
||||
while line.is_ok() {
|
||||
message.push_str(line.unwrap().as_slice());
|
||||
line = stdin().read_line();
|
||||
}
|
||||
|
||||
match sendmail(
|
||||
// sender
|
||||
matches.opt_str("r").unwrap().clone(),
|
||||
// recipients
|
||||
recipients,
|
||||
// message content
|
||||
message,
|
||||
// subject
|
||||
match matches.opt_str("s") {
|
||||
Some(ref subject) => subject.clone(),
|
||||
None => "(empty subject)".to_string(),
|
||||
},
|
||||
// server
|
||||
match matches.opt_str("a") {
|
||||
Some(ref server) => server.clone(),
|
||||
None => "localhost".to_string(),
|
||||
},
|
||||
// port
|
||||
match matches.opt_str("p") {
|
||||
Some(port) => port.as_slice().parse::<u16>().unwrap(),
|
||||
None => 25,
|
||||
},
|
||||
// my hostname
|
||||
match matches.opt_str("m") {
|
||||
Some(ref my_hostname) => my_hostname.clone(),
|
||||
None => "localhost".to_string(),
|
||||
},
|
||||
// number of copies
|
||||
match matches.opt_str("n") {
|
||||
Some(ref n) => n.as_slice().parse::<u16>().unwrap(),
|
||||
None => 1,
|
||||
},
|
||||
)
|
||||
{
|
||||
Ok(..) => info!("Email sent successfully"),
|
||||
Err(error) => error!("{}", error),
|
||||
}
|
||||
}
|
||||
1
lettre/CHANGELOG.md
Symbolic link
1
lettre/CHANGELOG.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../CHANGELOG.md
|
||||
53
lettre/Cargo.toml
Normal file
53
lettre/Cargo.toml
Normal file
@@ -0,0 +1,53 @@
|
||||
[package]
|
||||
|
||||
name = "lettre"
|
||||
version = "0.9.2" # remember to update html_root_url
|
||||
description = "Email client"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "smtp", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre" }
|
||||
appveyor = { repository = "lettre/lettre" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre" }
|
||||
|
||||
[dependencies]
|
||||
log = "^0.4"
|
||||
nom = { version = "^4.0", optional = true }
|
||||
bufstream = { version = "^0.1", optional = true }
|
||||
native-tls = { version = "^0.2", optional = true }
|
||||
base64 = { version = "^0.10", optional = true }
|
||||
hostname = { version = "^0.1", optional = true }
|
||||
serde = { version = "^1.0", optional = true }
|
||||
serde_json = { version = "^1.0", optional = true }
|
||||
serde_derive = { version = "^1.0", optional = true }
|
||||
fast_chemail = "^0.9"
|
||||
r2d2 = { version = "^0.8", optional = true}
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "^0.6"
|
||||
glob = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["file-transport", "smtp-transport", "sendmail-transport"]
|
||||
unstable = []
|
||||
serde-impls = ["serde", "serde_derive"]
|
||||
file-transport = ["serde-impls", "serde_json"]
|
||||
smtp-transport = ["bufstream", "native-tls", "base64", "nom", "hostname"]
|
||||
sendmail-transport = []
|
||||
connection-pool = [ "r2d2" ]
|
||||
|
||||
[[example]]
|
||||
name = "smtp"
|
||||
required-features = ["smtp-transport"]
|
||||
|
||||
[[example]]
|
||||
name = "smtp_gmail"
|
||||
required-features = ["smtp-transport"]
|
||||
1
lettre/LICENSE
Symbolic link
1
lettre/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
1
lettre/README.md
Symbolic link
1
lettre/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../README.md
|
||||
50
lettre/benches/transport_smtp.rs
Normal file
50
lettre/benches/transport_smtp.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate lettre;
|
||||
extern crate test;
|
||||
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use lettre::{ClientSecurity, Envelope, SmtpTransport};
|
||||
use lettre::{EmailAddress, SendableEmail, Transport};
|
||||
|
||||
#[bench]
|
||||
fn bench_simple_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
#[bench]
|
||||
fn bench_reuse_send(b: &mut test::Bencher) {
|
||||
let mut sender = SmtpTransport::builder("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited)
|
||||
.build();
|
||||
b.iter(|| {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
});
|
||||
sender.close()
|
||||
}
|
||||
31
lettre/examples/smtp.rs
Normal file
31
lettre/examples/smtp.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
extern crate env_logger;
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
38
lettre/examples/smtp_gmail.rs
Normal file
38
lettre/examples/smtp_gmail.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::Credentials;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("from@gmail.com".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.com".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let creds = Credentials::new(
|
||||
"example_username".to_string(),
|
||||
"example_password".to_string(),
|
||||
);
|
||||
|
||||
// Open a remote connection to gmail
|
||||
let mut mailer = SmtpClient::new_simple("smtp.gmail.com")
|
||||
.unwrap()
|
||||
.credentials(creds)
|
||||
.transport();
|
||||
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
35
lettre/src/error.rs
Normal file
35
lettre/src/error.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use self::Error::*;
|
||||
|
||||
/// Error type for email content
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Error {
|
||||
/// Missing from in envelope
|
||||
MissingFrom,
|
||||
/// Missing to in envelope
|
||||
MissingTo,
|
||||
/// Invalid email
|
||||
InvalidEmailAddress,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
MissingFrom => "missing source address, invalid envelope".to_owned(),
|
||||
MissingTo => "missing destination address, invalid envelope".to_owned(),
|
||||
InvalidEmailAddress => "invalid email address".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Email result type
|
||||
pub type EmailResult<T> = Result<T, Error>;
|
||||
65
lettre/src/file/error.rs
Normal file
65
lettre/src/file/error.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
//! Error and result type for file transport
|
||||
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use serde_json;
|
||||
use std::io;
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// JSON serialization error
|
||||
JsonSerialization(serde_json::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
JsonSerialization(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
JsonSerialization(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(err: serde_json::Error) -> Error {
|
||||
Error::JsonSerialization(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type FileResult = Result<(), Error>;
|
||||
60
lettre/src/file/mod.rs
Normal file
60
lettre/src/file/mod.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! The file transport writes the emails to the given directory. The name of the file will be
|
||||
//! `message_id.txt`.
|
||||
//! It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
//!
|
||||
|
||||
use file::error::FileResult;
|
||||
use serde_json;
|
||||
use std::fs::File;
|
||||
use std::io::prelude::*;
|
||||
use std::path::{Path, PathBuf};
|
||||
use Envelope;
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Writes the content and the envelope information to a file
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct FileTransport {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileTransport {
|
||||
/// Creates a new transport to the given directory
|
||||
pub fn new<P: AsRef<Path>>(path: P) -> FileTransport {
|
||||
FileTransport {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
struct SerializableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for FileTransport {
|
||||
type Result = FileResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> FileResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
let envelope = email.envelope().clone();
|
||||
|
||||
let mut file = self.path.clone();
|
||||
file.push(format!("{}.json", message_id));
|
||||
|
||||
let serialized = serde_json::to_string(&SerializableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: email.message_to_string()?.as_bytes().to_vec(),
|
||||
})?;
|
||||
|
||||
File::create(file.as_path())?.write_all(serialized.as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
213
lettre/src/lib.rs
Normal file
213
lettre/src/lib.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
//! Lettre is a mailer written in Rust. It provides a simple email builder and several transports.
|
||||
//!
|
||||
//! This mailer contains the available transports for your emails.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre/0.9.2")]
|
||||
#![deny(
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate base64;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate bufstream;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate hostname;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
extern crate native_tls;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
#[macro_use]
|
||||
extern crate nom;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
extern crate serde;
|
||||
#[cfg(feature = "serde-impls")]
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[cfg(feature = "file-transport")]
|
||||
extern crate serde_json;
|
||||
extern crate fast_chemail;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
extern crate r2d2;
|
||||
|
||||
pub mod error;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub mod file;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub mod sendmail;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub mod smtp;
|
||||
pub mod stub;
|
||||
|
||||
use error::EmailResult;
|
||||
use error::Error;
|
||||
use fast_chemail::is_valid_email;
|
||||
#[cfg(feature = "file-transport")]
|
||||
pub use file::FileTransport;
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
pub use sendmail::SendmailTransport;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::client::net::ClientTlsParameters;
|
||||
#[cfg(all(feature = "smtp-transport", feature = "connection-pool"))]
|
||||
pub use smtp::r2d2::SmtpConnectionManager;
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
pub use smtp::{ClientSecurity, SmtpClient, SmtpTransport};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::io;
|
||||
use std::io::Cursor;
|
||||
use std::io::Read;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// Email address
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EmailAddress(String);
|
||||
|
||||
impl EmailAddress {
|
||||
pub fn new(address: String) -> EmailResult<EmailAddress> {
|
||||
if !is_valid_email(&address) && !address.ends_with("localhost") {
|
||||
Err(Error::InvalidEmailAddress)?;
|
||||
}
|
||||
Ok(EmailAddress(address))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EmailAddress {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
EmailAddress::new(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for EmailAddress {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for EmailAddress {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<OsStr> for EmailAddress {
|
||||
fn as_ref(&self) -> &OsStr {
|
||||
&self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple email envelope representation
|
||||
///
|
||||
/// We only accept mailboxes, and do not support source routes (as per RFC).
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct Envelope {
|
||||
/// The envelope recipients' addresses
|
||||
///
|
||||
/// This can not be empty.
|
||||
forward_path: Vec<EmailAddress>,
|
||||
/// The envelope sender address
|
||||
reverse_path: Option<EmailAddress>,
|
||||
}
|
||||
|
||||
impl Envelope {
|
||||
/// Creates a new envelope, which may fail if `to` is empty.
|
||||
pub fn new(from: Option<EmailAddress>, to: Vec<EmailAddress>) -> EmailResult<Envelope> {
|
||||
if to.is_empty() {
|
||||
Err(Error::MissingTo)?;
|
||||
}
|
||||
Ok(Envelope {
|
||||
forward_path: to,
|
||||
reverse_path: from,
|
||||
})
|
||||
}
|
||||
|
||||
/// Destination addresses of the envelope
|
||||
pub fn to(&self) -> &[EmailAddress] {
|
||||
self.forward_path.as_slice()
|
||||
}
|
||||
|
||||
/// Source address of the envelope
|
||||
pub fn from(&self) -> Option<&EmailAddress> {
|
||||
self.reverse_path.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Reader(Box<Read + Send>),
|
||||
Bytes(Cursor<Vec<u8>>),
|
||||
}
|
||||
|
||||
impl Read for Message {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
Message::Reader(ref mut rdr) => rdr.read(buf),
|
||||
Message::Bytes(ref mut rdr) => rdr.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendable email structure
|
||||
pub struct SendableEmail {
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Message,
|
||||
}
|
||||
|
||||
impl SendableEmail {
|
||||
pub fn new(envelope: Envelope, message_id: String, message: Vec<u8>) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Bytes(Cursor::new(message)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_reader(
|
||||
envelope: Envelope,
|
||||
message_id: String,
|
||||
message: Box<Read + Send>,
|
||||
) -> SendableEmail {
|
||||
SendableEmail {
|
||||
envelope,
|
||||
message_id,
|
||||
message: Message::Reader(message),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn envelope(&self) -> &Envelope {
|
||||
&self.envelope
|
||||
}
|
||||
|
||||
pub fn message_id(&self) -> &str {
|
||||
&self.message_id
|
||||
}
|
||||
|
||||
pub fn message(self) -> Message {
|
||||
self.message
|
||||
}
|
||||
|
||||
pub fn message_to_string(mut self) -> Result<String, io::Error> {
|
||||
let mut message_content = String::new();
|
||||
self.message.read_to_string(&mut message_content)?;
|
||||
Ok(message_content)
|
||||
}
|
||||
}
|
||||
|
||||
/// Transport method for emails
|
||||
pub trait Transport<'a> {
|
||||
/// Result type for the transport
|
||||
type Result;
|
||||
|
||||
/// Sends the email
|
||||
fn send(&mut self, email: SendableEmail) -> Self::Result;
|
||||
}
|
||||
54
lettre/src/sendmail/error.rs
Normal file
54
lettre/src/sendmail/error.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! Error and result type for sendmail transport
|
||||
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use self::Error::*;
|
||||
use std::io;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
Io(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Error::Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// sendmail result type
|
||||
pub type SendmailResult = Result<(), Error>;
|
||||
78
lettre/src/sendmail/mod.rs
Normal file
78
lettre/src/sendmail/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! The sendmail transport sends the email using the local sendmail command.
|
||||
//!
|
||||
|
||||
use sendmail::error::SendmailResult;
|
||||
use std::io::prelude::*;
|
||||
use std::io::Read;
|
||||
use std::process::{Command, Stdio};
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
pub mod error;
|
||||
|
||||
/// Sends an email using the `sendmail` command
|
||||
#[derive(Debug, Default)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct SendmailTransport {
|
||||
command: String,
|
||||
}
|
||||
|
||||
impl SendmailTransport {
|
||||
/// Creates a new transport with the default `/usr/sbin/sendmail` command
|
||||
pub fn new() -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: "/usr/sbin/sendmail".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new transport to the given sendmail command
|
||||
pub fn new_with_command<S: Into<String>>(command: S) -> SendmailTransport {
|
||||
SendmailTransport {
|
||||
command: command.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SendmailTransport {
|
||||
type Result = SendmailResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> SendmailResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
// Spawn the sendmail command
|
||||
let mut process = Command::new(&self.command)
|
||||
.arg("-i")
|
||||
.arg("-f")
|
||||
.arg(
|
||||
email
|
||||
.envelope()
|
||||
.from()
|
||||
.map(|x| x.as_ref())
|
||||
.unwrap_or("\"\""),
|
||||
)
|
||||
.args(email.envelope.to())
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
|
||||
let mut message_content = String::new();
|
||||
let _ = email.message().read_to_string(&mut message_content);
|
||||
|
||||
process
|
||||
.stdin
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.write_all(message_content.as_bytes())?;
|
||||
|
||||
info!("Wrote {} message to stdin", message_id);
|
||||
|
||||
let output = process.wait_with_output()?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
// TODO display stderr
|
||||
Err(error::Error::Client("The message could not be sent"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
178
lettre/src/smtp/authentication.rs
Normal file
178
lettre/src/smtp/authentication.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
//! Provides limited SASL authentication mechanisms
|
||||
|
||||
use smtp::error::Error;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Accepted authentication mechanisms on an encrypted connection
|
||||
/// Trying LOGIN last as it is deprecated.
|
||||
pub const DEFAULT_ENCRYPTED_MECHANISMS: &[Mechanism] = &[Mechanism::Plain, Mechanism::Login];
|
||||
|
||||
/// Accepted authentication mechanisms on an unencrypted connection
|
||||
pub const DEFAULT_UNENCRYPTED_MECHANISMS: &[Mechanism] = &[];
|
||||
|
||||
/// Convertable to user credentials
|
||||
pub trait IntoCredentials {
|
||||
/// Converts to a `Credentials` struct
|
||||
fn into_credentials(self) -> Credentials;
|
||||
}
|
||||
|
||||
impl IntoCredentials for Credentials {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Into<String>, T: Into<String>> IntoCredentials for (S, T) {
|
||||
fn into_credentials(self) -> Credentials {
|
||||
let (username, password) = self;
|
||||
Credentials::new(username.into(), password.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains user credentials
|
||||
#[derive(PartialEq, Eq, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct Credentials {
|
||||
authentication_identity: String,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
impl Credentials {
|
||||
/// Create a `Credentials` struct from username and password
|
||||
pub fn new(username: String, password: String) -> Credentials {
|
||||
Credentials {
|
||||
authentication_identity: username,
|
||||
secret: password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents authentication mechanisms
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Hash, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum Mechanism {
|
||||
/// PLAIN authentication mechanism
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
Plain,
|
||||
/// LOGIN authentication mechanism
|
||||
/// Obsolete but needed for some providers (like office365)
|
||||
/// https://www.ietf.org/archive/id/draft-murchison-sasl-login-00.txt
|
||||
Login,
|
||||
/// Non-standard XOAUTH2 mechanism
|
||||
/// https://developers.google.com/gmail/imap/xoauth2-protocol
|
||||
Xoauth2,
|
||||
}
|
||||
|
||||
impl Display for Mechanism {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match *self {
|
||||
Mechanism::Plain => "PLAIN",
|
||||
Mechanism::Login => "LOGIN",
|
||||
Mechanism::Xoauth2 => "XOAUTH2",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
/// Does the mechanism supports initial response
|
||||
pub fn supports_initial_response(self) -> bool {
|
||||
match self {
|
||||
Mechanism::Plain | Mechanism::Xoauth2 => true,
|
||||
Mechanism::Login => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string to send to the server, using the provided username, password and
|
||||
/// challenge in some cases
|
||||
pub fn response(
|
||||
self,
|
||||
credentials: &Credentials,
|
||||
challenge: Option<&str>,
|
||||
) -> Result<String, Error> {
|
||||
match self {
|
||||
Mechanism::Plain => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"\u{0}{}\u{0}{}",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
Mechanism::Login => {
|
||||
let decoded_challenge =
|
||||
challenge.ok_or(Error::Client("This mechanism does expect a challenge"))?;
|
||||
|
||||
if vec!["User Name", "Username:", "Username"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.authentication_identity.to_string());
|
||||
}
|
||||
|
||||
if vec!["Password", "Password:"].contains(&decoded_challenge) {
|
||||
return Ok(credentials.secret.to_string());
|
||||
}
|
||||
|
||||
Err(Error::Client("Unrecognized challenge"))
|
||||
}
|
||||
Mechanism::Xoauth2 => match challenge {
|
||||
Some(_) => Err(Error::Client("This mechanism does not expect a challenge")),
|
||||
None => Ok(format!(
|
||||
"user={}\x01auth=Bearer {}\x01\x01",
|
||||
credentials.authentication_identity, credentials.secret
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Credentials, Mechanism};
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
let mechanism = Mechanism::Plain;
|
||||
|
||||
let credentials = Credentials::new("username".to_string(), "password".to_string());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"\u{0}username\u{0}password"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_login() {
|
||||
let mechanism = Mechanism::Login;
|
||||
|
||||
let credentials = Credentials::new("alice".to_string(), "wonderland".to_string());
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Username")).unwrap(),
|
||||
"alice"
|
||||
);
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, Some("Password")).unwrap(),
|
||||
"wonderland"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, None).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xoauth2() {
|
||||
let mechanism = Mechanism::Xoauth2;
|
||||
|
||||
let credentials = Credentials::new(
|
||||
"username".to_string(),
|
||||
"vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==".to_string(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
mechanism.response(&credentials, None).unwrap(),
|
||||
"user=username\x01auth=Bearer vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg==\x01\x01"
|
||||
);
|
||||
assert!(mechanism.response(&credentials, Some("test")).is_err());
|
||||
}
|
||||
}
|
||||
120
lettre/src/smtp/client/mock.rs
Normal file
120
lettre/src/smtp/client/mock.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
#![allow(missing_docs)]
|
||||
// Comes from https://github.com/inre/rust-mq/blob/master/netopt
|
||||
|
||||
use std::io::{self, Cursor, Read, Write};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub type MockCursor = Cursor<Vec<u8>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockStream {
|
||||
reader: Arc<Mutex<MockCursor>>,
|
||||
writer: Arc<Mutex<MockCursor>>,
|
||||
}
|
||||
|
||||
impl Default for MockStream {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MockStream {
|
||||
pub fn new() -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_vec(vec: Vec<u8>) -> MockStream {
|
||||
MockStream {
|
||||
reader: Arc::new(Mutex::new(MockCursor::new(vec))),
|
||||
writer: Arc::new(Mutex::new(MockCursor::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_vec(&mut self) -> Vec<u8> {
|
||||
let mut cursor = self.writer.lock().unwrap();
|
||||
let vec = cursor.get_ref().to_vec();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn next_vec(&mut self, vec: &[u8]) {
|
||||
let mut cursor = self.reader.lock().unwrap();
|
||||
cursor.set_position(0);
|
||||
cursor.get_mut().clear();
|
||||
cursor.get_mut().extend_from_slice(vec);
|
||||
}
|
||||
|
||||
pub fn swap(&mut self) {
|
||||
let mut cur_write = self.writer.lock().unwrap();
|
||||
let mut cur_read = self.reader.lock().unwrap();
|
||||
let vec_write = cur_write.get_ref().to_vec();
|
||||
let vec_read = cur_read.get_ref().to_vec();
|
||||
cur_write.set_position(0);
|
||||
cur_read.set_position(0);
|
||||
cur_write.get_mut().clear();
|
||||
cur_read.get_mut().clear();
|
||||
// swap cursors
|
||||
cur_read.get_mut().extend_from_slice(vec_write.as_slice());
|
||||
cur_write.get_mut().extend_from_slice(vec_read.as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for MockStream {
|
||||
fn write(&mut self, msg: &[u8]) -> io::Result<usize> {
|
||||
self.writer.lock().unwrap().write(msg)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.lock().unwrap().flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for MockStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.reader.lock().unwrap().read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::MockStream;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn write_take_test() {
|
||||
let mut mock = MockStream::new();
|
||||
// write to mock stream
|
||||
mock.write(&[1, 2, 3]).unwrap();
|
||||
assert_eq!(mock.take_vec(), vec![1, 2, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_with_vec_test() {
|
||||
let mut mock = MockStream::with_vec(vec![4, 5]);
|
||||
let mut vec = Vec::new();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![4, 5]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut cloned = mock.clone();
|
||||
mock.write(&[6, 7]).unwrap();
|
||||
assert_eq!(cloned.take_vec(), vec![6, 7]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_test() {
|
||||
let mut mock = MockStream::new();
|
||||
let mut vec = Vec::new();
|
||||
mock.write(&[8, 9, 10]).unwrap();
|
||||
mock.swap();
|
||||
mock.read_to_end(&mut vec).unwrap();
|
||||
assert_eq!(vec, vec![8, 9, 10]);
|
||||
}
|
||||
}
|
||||
312
lettre/src/smtp/client/mod.rs
Normal file
312
lettre/src/smtp/client/mod.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
//! SMTP client
|
||||
|
||||
use bufstream::BufStream;
|
||||
use nom::ErrorKind as NomErrorKind;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::client::net::{ClientTlsParameters, Connector, NetworkStream, Timeout};
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::io::{self, BufRead, BufReader, Read, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::string::String;
|
||||
use std::time::Duration;
|
||||
|
||||
pub mod mock;
|
||||
pub mod net;
|
||||
|
||||
/// The codec used for transparency
|
||||
#[derive(Default, Clone, Copy, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ClientCodec {
|
||||
escape_count: u8,
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Creates a new client codec
|
||||
pub fn new() -> Self {
|
||||
ClientCodec::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientCodec {
|
||||
/// Adds transparency
|
||||
/// TODO: replace CR and LF by CRLF
|
||||
fn encode(&mut self, frame: &[u8], buf: &mut Vec<u8>) -> Result<(), Error> {
|
||||
match frame.len() {
|
||||
0 => {
|
||||
match self.escape_count {
|
||||
0 => buf.write_all(b"\r\n.\r\n")?,
|
||||
1 => buf.write_all(b"\n.\r\n")?,
|
||||
2 => buf.write_all(b".\r\n")?,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
self.escape_count = 0;
|
||||
Ok(())
|
||||
}
|
||||
_ => {
|
||||
let mut start = 0;
|
||||
for (idx, byte) in frame.iter().enumerate() {
|
||||
match self.escape_count {
|
||||
0 => self.escape_count = if *byte == b'\r' { 1 } else { 0 },
|
||||
1 => self.escape_count = if *byte == b'\n' { 2 } else { 0 },
|
||||
2 => self.escape_count = if *byte == b'.' { 3 } else { 0 },
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if self.escape_count == 3 {
|
||||
self.escape_count = 0;
|
||||
buf.write_all(&frame[start..idx])?;
|
||||
buf.write_all(b".")?;
|
||||
start = idx;
|
||||
}
|
||||
}
|
||||
buf.write_all(&frame[start..])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the string replacing all the CRLF with "\<CRLF\>"
|
||||
/// Used for debug displays
|
||||
fn escape_crlf(string: &str) -> String {
|
||||
string.replace("\r\n", "<CRLF>")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
#[derive(Debug, Default)]
|
||||
pub struct InnerClient<S: Write + Read = NetworkStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
}
|
||||
|
||||
macro_rules! return_err (
|
||||
($err: expr, $client: ident) => ({
|
||||
return Err(From::from($err))
|
||||
})
|
||||
);
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::new_without_default_derive))]
|
||||
impl<S: Write + Read> InnerClient<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new() -> InnerClient<S> {
|
||||
InnerClient { stream: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connector + Write + Read + Timeout + Debug> InnerClient<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.command(QuitCommand);
|
||||
self.stream = None;
|
||||
}
|
||||
|
||||
/// Sets the underlying stream
|
||||
pub fn set_stream(&mut self, stream: S) {
|
||||
self.stream = Some(BufStream::new(stream));
|
||||
}
|
||||
|
||||
/// Upgrades the underlying connection to SSL/TLS
|
||||
pub fn upgrade_tls_stream(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => stream.get_mut().upgrade_tls(tls_parameters),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the underlying stream is currently encrypted
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
match self.stream {
|
||||
Some(ref stream) => stream.get_ref().is_encrypted(),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set timeout
|
||||
pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match self.stream {
|
||||
Some(ref mut stream) => {
|
||||
stream.get_mut().set_read_timeout(duration)?;
|
||||
stream.get_mut().set_write_timeout(duration)?;
|
||||
Ok(())
|
||||
}
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect<A: ToSocketAddrs>(
|
||||
&mut self,
|
||||
addr: &A,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> SmtpResult {
|
||||
// Connect should not be called when the client is already connected
|
||||
if self.stream.is_some() {
|
||||
return_err!("The connection is already established", self);
|
||||
}
|
||||
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
let server_addr = match addresses.next() {
|
||||
Some(addr) => addr,
|
||||
None => return_err!("Could not resolve hostname", self),
|
||||
};
|
||||
|
||||
debug!("connecting to {}", server_addr);
|
||||
|
||||
// Try to connect
|
||||
self.set_stream(Connector::connect(&server_addr, tls_parameters)?);
|
||||
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Checks if the server is connected using the NOOP SMTP command
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::wrong_self_convention))]
|
||||
pub fn is_connected(&mut self) -> bool {
|
||||
self.stream.is_some() && self.command(NoopCommand).is_ok()
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with the given mechanism, and handles challenge if needed
|
||||
pub fn auth(&mut self, mechanism: Mechanism, credentials: &Credentials) -> SmtpResult {
|
||||
// TODO
|
||||
let mut challenges = 10;
|
||||
let mut response = self.command(AuthCommand::new(mechanism, credentials.clone(), None)?)?;
|
||||
|
||||
while challenges > 0 && response.has_code(334) {
|
||||
challenges -= 1;
|
||||
response = self.command(AuthCommand::new_from_response(
|
||||
mechanism,
|
||||
credentials.clone(),
|
||||
&response,
|
||||
)?)?;
|
||||
}
|
||||
|
||||
if challenges == 0 {
|
||||
Err(Error::ResponseParsing("Unexpected number of challenges"))
|
||||
} else {
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends the message content
|
||||
pub fn message(&mut self, message: Box<Read>) -> SmtpResult {
|
||||
let mut out_buf: Vec<u8> = vec![];
|
||||
let mut codec = ClientCodec::new();
|
||||
|
||||
let mut message_reader = BufReader::new(message);
|
||||
|
||||
loop {
|
||||
out_buf.clear();
|
||||
|
||||
let consumed = match message_reader.fill_buf() {
|
||||
Ok(bytes) => {
|
||||
codec.encode(bytes, &mut out_buf)?;
|
||||
bytes.len()
|
||||
}
|
||||
Err(ref err) => panic!("Failed with: {}", err),
|
||||
};
|
||||
message_reader.consume(consumed);
|
||||
|
||||
if consumed == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
self.write(out_buf.as_slice())?;
|
||||
}
|
||||
|
||||
self.write(b"\r\n.\r\n")?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Sends an SMTP command
|
||||
pub fn command<C: Display>(&mut self, command: C) -> SmtpResult {
|
||||
self.write(command.to_string().as_bytes())?;
|
||||
self.read_response()
|
||||
}
|
||||
|
||||
/// Writes a string to the server
|
||||
fn write(&mut self, string: &[u8]) -> Result<(), Error> {
|
||||
if self.stream.is_none() {
|
||||
return Err(From::from("Connection closed"));
|
||||
}
|
||||
|
||||
self.stream.as_mut().unwrap().write_all(string)?;
|
||||
self.stream.as_mut().unwrap().flush()?;
|
||||
|
||||
debug!(
|
||||
"Wrote: {}",
|
||||
escape_crlf(String::from_utf8_lossy(string).as_ref())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the SMTP response
|
||||
fn read_response(&mut self) -> SmtpResult {
|
||||
let mut raw_response = String::new();
|
||||
let mut response = raw_response.parse::<Response>();
|
||||
|
||||
while response.is_err() {
|
||||
if response.as_ref().err().unwrap() != &NomErrorKind::Complete {
|
||||
break;
|
||||
}
|
||||
// TODO read more than one line
|
||||
let read_count = self.stream.as_mut().unwrap().read_line(&mut raw_response)?;
|
||||
|
||||
// EOF is reached
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
response = raw_response.parse::<Response>();
|
||||
}
|
||||
|
||||
debug!("Read: {}", escape_crlf(raw_response.as_ref()));
|
||||
|
||||
let final_response = response?;
|
||||
|
||||
if final_response.is_positive() {
|
||||
Ok(final_response)
|
||||
} else {
|
||||
Err(From::from(final_response))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_crlf, ClientCodec};
|
||||
|
||||
#[test]
|
||||
fn test_codec() {
|
||||
let mut codec = ClientCodec::new();
|
||||
let mut buf: Vec<u8> = vec![];
|
||||
|
||||
assert!(codec.encode(b"test\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".\r\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"\r\ntest", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"te\r\n.\r\nst", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test.", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b".test\n", &mut buf).is_ok());
|
||||
assert!(codec.encode(b"test", &mut buf).is_ok());
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
"test\r\n..\r\n\r\ntestte\r\n..\r\nsttesttest.test\n.test\ntest"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n"), "<CRLF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n"), "EHLO my_name<CRLF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n"),
|
||||
"EHLO my_name<CRLF>SIZE 42<CRLF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
172
lettre/src/smtp/client/net.rs
Normal file
172
lettre/src/smtp/client/net.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
//! A trait to represent a stream
|
||||
|
||||
use native_tls::{Protocol, TlsConnector, TlsStream};
|
||||
use smtp::client::mock::MockStream;
|
||||
use std::io::{self, ErrorKind, Read, Write};
|
||||
use std::net::{Ipv4Addr, Shutdown, SocketAddr, SocketAddrV4, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Parameters to use for secure clients
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct ClientTlsParameters {
|
||||
/// A connector from `native-tls`
|
||||
pub connector: TlsConnector,
|
||||
/// The domain to send during the TLS handshake
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
impl ClientTlsParameters {
|
||||
/// Creates a `ClientTlsParameters`
|
||||
pub fn new(domain: String, connector: TlsConnector) -> ClientTlsParameters {
|
||||
ClientTlsParameters { connector, domain }
|
||||
}
|
||||
}
|
||||
|
||||
/// Accepted protocols by default.
|
||||
/// This removes TLS 1.0 and 1.1 compared to tls-native defaults.
|
||||
pub const DEFAULT_TLS_PROTOCOLS: &[Protocol] = &[Protocol::Tlsv12];
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Represents the different types of underlying network streams
|
||||
pub enum NetworkStream {
|
||||
/// Plain TCP stream
|
||||
Tcp(TcpStream),
|
||||
/// Encrypted TCP stream
|
||||
Tls(TlsStream<TcpStream>),
|
||||
/// Mock stream
|
||||
Mock(MockStream),
|
||||
}
|
||||
|
||||
impl NetworkStream {
|
||||
/// Returns peer's address
|
||||
pub fn peer_addr(&self) -> io::Result<SocketAddr> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.peer_addr(),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().peer_addr(),
|
||||
NetworkStream::Mock(_) => Ok(SocketAddr::V4(SocketAddrV4::new(
|
||||
Ipv4Addr::new(127, 0, 0, 1),
|
||||
80,
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shutdowns the connection
|
||||
pub fn shutdown(&self, how: Shutdown) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref s) => s.shutdown(how),
|
||||
NetworkStream::Tls(ref s) => s.get_ref().shutdown(how),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Read for NetworkStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.read(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.read(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.read(buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Write for NetworkStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.write(buf),
|
||||
NetworkStream::Tls(ref mut s) => s.write(buf),
|
||||
NetworkStream::Mock(ref mut s) => s.write(buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut s) => s.flush(),
|
||||
NetworkStream::Tls(ref mut s) => s.flush(),
|
||||
NetworkStream::Mock(ref mut s) => s.flush(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connector: Sized {
|
||||
/// Opens a connection to the given IP socket
|
||||
fn connect(addr: &SocketAddr, tls_parameters: Option<&ClientTlsParameters>)
|
||||
-> io::Result<Self>;
|
||||
/// Upgrades to TLS connection
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()>;
|
||||
/// Is the NetworkStream encrypted
|
||||
fn is_encrypted(&self) -> bool;
|
||||
}
|
||||
|
||||
impl Connector for NetworkStream {
|
||||
fn connect(
|
||||
addr: &SocketAddr,
|
||||
tls_parameters: Option<&ClientTlsParameters>,
|
||||
) -> io::Result<NetworkStream> {
|
||||
let tcp_stream = TcpStream::connect(addr)?;
|
||||
|
||||
match tls_parameters {
|
||||
Some(context) => context
|
||||
.connector
|
||||
.connect(context.domain.as_ref(), tcp_stream)
|
||||
.map(NetworkStream::Tls)
|
||||
.map_err(|e| io::Error::new(ErrorKind::Other, e)),
|
||||
None => Ok(NetworkStream::Tcp(tcp_stream)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn upgrade_tls(&mut self, tls_parameters: &ClientTlsParameters) -> io::Result<()> {
|
||||
*self = match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => match tls_parameters
|
||||
.connector
|
||||
.connect(tls_parameters.domain.as_ref(), stream.try_clone().unwrap())
|
||||
{
|
||||
Ok(tls_stream) => NetworkStream::Tls(tls_stream),
|
||||
Err(err) => return Err(io::Error::new(ErrorKind::Other, err)),
|
||||
},
|
||||
NetworkStream::Tls(_) => return Ok(()),
|
||||
NetworkStream::Mock(_) => return Ok(()),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn is_encrypted(&self) -> bool {
|
||||
match *self {
|
||||
NetworkStream::Tcp(_) => false,
|
||||
NetworkStream::Tls(_) => true,
|
||||
NetworkStream::Mock(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for read and write timeout support
|
||||
pub trait Timeout: Sized {
|
||||
/// Set read timeout for IO calls
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()>;
|
||||
}
|
||||
|
||||
impl Timeout for NetworkStream {
|
||||
fn set_read_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_read_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_read_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set write timeout for IO calls
|
||||
fn set_write_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
|
||||
match *self {
|
||||
NetworkStream::Tcp(ref mut stream) => stream.set_write_timeout(duration),
|
||||
NetworkStream::Tls(ref mut stream) => stream.get_ref().set_write_timeout(duration),
|
||||
NetworkStream::Mock(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
378
lettre/src/smtp/commands.rs
Normal file
378
lettre/src/smtp/commands.rs
Normal file
@@ -0,0 +1,378 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
|
||||
//! SMTP commands
|
||||
|
||||
use base64;
|
||||
use smtp::authentication::{Credentials, Mechanism};
|
||||
use smtp::error::Error;
|
||||
use smtp::extension::ClientId;
|
||||
use smtp::extension::{MailParameter, RcptParameter};
|
||||
use smtp::response::Response;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use EmailAddress;
|
||||
|
||||
/// EHLO command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct EhloCommand {
|
||||
client_id: ClientId,
|
||||
}
|
||||
|
||||
impl Display for EhloCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EHLO {}\r\n", self.client_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl EhloCommand {
|
||||
/// Creates a EHLO command
|
||||
pub fn new(client_id: ClientId) -> EhloCommand {
|
||||
EhloCommand { client_id }
|
||||
}
|
||||
}
|
||||
|
||||
/// STARTTLS command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct StarttlsCommand;
|
||||
|
||||
impl Display for StarttlsCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("STARTTLS\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// MAIL command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct MailCommand {
|
||||
sender: Option<EmailAddress>,
|
||||
parameters: Vec<MailParameter>,
|
||||
}
|
||||
|
||||
impl Display for MailCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"MAIL FROM:<{}>",
|
||||
self.sender.as_ref().map(|x| x.as_ref()).unwrap_or("")
|
||||
)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl MailCommand {
|
||||
/// Creates a MAIL command
|
||||
pub fn new(sender: Option<EmailAddress>, parameters: Vec<MailParameter>) -> MailCommand {
|
||||
MailCommand { sender, parameters }
|
||||
}
|
||||
}
|
||||
|
||||
/// RCPT command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RcptCommand {
|
||||
recipient: EmailAddress,
|
||||
parameters: Vec<RcptParameter>,
|
||||
}
|
||||
|
||||
impl Display for RcptCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "RCPT TO:<{}>", self.recipient)?;
|
||||
for parameter in &self.parameters {
|
||||
write!(f, " {}", parameter)?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl RcptCommand {
|
||||
/// Creates an RCPT command
|
||||
pub fn new(recipient: EmailAddress, parameters: Vec<RcptParameter>) -> RcptCommand {
|
||||
RcptCommand {
|
||||
recipient,
|
||||
parameters,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// DATA command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct DataCommand;
|
||||
|
||||
impl Display for DataCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("DATA\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// QUIT command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct QuitCommand;
|
||||
|
||||
impl Display for QuitCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("QUIT\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// NOOP command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct NoopCommand;
|
||||
|
||||
impl Display for NoopCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("NOOP\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// HELP command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct HelpCommand {
|
||||
argument: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for HelpCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("HELP")?;
|
||||
if self.argument.is_some() {
|
||||
write!(f, " {}", self.argument.as_ref().unwrap())?;
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl HelpCommand {
|
||||
/// Creates an HELP command
|
||||
pub fn new(argument: Option<String>) -> HelpCommand {
|
||||
HelpCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// VRFY command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct VrfyCommand {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for VrfyCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::write_with_newline))]
|
||||
write!(f, "VRFY {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl VrfyCommand {
|
||||
/// Creates a VRFY command
|
||||
pub fn new(argument: String) -> VrfyCommand {
|
||||
VrfyCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// EXPN command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct ExpnCommand {
|
||||
argument: String,
|
||||
}
|
||||
|
||||
impl Display for ExpnCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "EXPN {}\r\n", self.argument)
|
||||
}
|
||||
}
|
||||
|
||||
impl ExpnCommand {
|
||||
/// Creates an EXPN command
|
||||
pub fn new(argument: String) -> ExpnCommand {
|
||||
ExpnCommand { argument }
|
||||
}
|
||||
}
|
||||
|
||||
/// RSET command
|
||||
#[derive(PartialEq, Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct RsetCommand;
|
||||
|
||||
impl Display for RsetCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
f.write_str("RSET\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// AUTH command
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub struct AuthCommand {
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
response: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for AuthCommand {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
let encoded_response = self
|
||||
.response
|
||||
.as_ref()
|
||||
.map(|r| base64::encode_config(r.as_bytes(), base64::STANDARD));
|
||||
|
||||
if self.mechanism.supports_initial_response() {
|
||||
write!(f, "AUTH {} {}", self.mechanism, encoded_response.unwrap())?;
|
||||
} else {
|
||||
match encoded_response {
|
||||
Some(response) => f.write_str(&response)?,
|
||||
None => write!(f, "AUTH {}", self.mechanism)?,
|
||||
}
|
||||
}
|
||||
f.write_str("\r\n")
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthCommand {
|
||||
/// Creates an AUTH command (from a challenge if provided)
|
||||
pub fn new(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
challenge: Option<String>,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
let response = if mechanism.supports_initial_response() || challenge.is_some() {
|
||||
Some(mechanism.response(&credentials, challenge.as_ref().map(String::as_str))?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(AuthCommand {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge,
|
||||
response,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates an AUTH command from a response that needs to be a
|
||||
/// valid challenge (with 334 response code)
|
||||
pub fn new_from_response(
|
||||
mechanism: Mechanism,
|
||||
credentials: Credentials,
|
||||
response: &Response,
|
||||
) -> Result<AuthCommand, Error> {
|
||||
if !response.has_code(334) {
|
||||
return Err(Error::ResponseParsing("Expecting a challenge"));
|
||||
}
|
||||
|
||||
let encoded_challenge = response
|
||||
.first_word()
|
||||
.ok_or(Error::ResponseParsing("Could not read auth challenge"))?;
|
||||
debug!("auth encoded challenge: {}", encoded_challenge);
|
||||
|
||||
let decoded_challenge = String::from_utf8(base64::decode(&encoded_challenge)?)?;
|
||||
debug!("auth decoded challenge: {}", decoded_challenge);
|
||||
|
||||
let response = Some(mechanism.response(&credentials, Some(decoded_challenge.as_ref()))?);
|
||||
|
||||
Ok(AuthCommand {
|
||||
mechanism,
|
||||
credentials,
|
||||
challenge: Some(decoded_challenge),
|
||||
response,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use smtp::extension::MailBodyParameter;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let id = ClientId::Domain("localhost".to_string());
|
||||
let email = EmailAddress::new("test@example.com".to_string()).unwrap();
|
||||
let mail_parameter = MailParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
let rcpt_parameter = RcptParameter::Other {
|
||||
keyword: "TEST".to_string(),
|
||||
value: Some("value".to_string()),
|
||||
};
|
||||
assert_eq!(format!("{}", EhloCommand::new(id)), "EHLO localhost\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(Some(email.clone()), vec![])),
|
||||
"MAIL FROM:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", MailCommand::new(None, vec![])),
|
||||
"MAIL FROM:<>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(Some(email.clone()), vec![MailParameter::Size(42)])
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
MailCommand::new(
|
||||
Some(email.clone()),
|
||||
vec![
|
||||
MailParameter::Size(42),
|
||||
MailParameter::Body(MailBodyParameter::EightBitMime),
|
||||
mail_parameter,
|
||||
],
|
||||
)
|
||||
),
|
||||
"MAIL FROM:<test@example.com> SIZE=42 BODY=8BITMIME TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![])),
|
||||
"RCPT TO:<test@example.com>\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", RcptCommand::new(email.clone(), vec![rcpt_parameter])),
|
||||
"RCPT TO:<test@example.com> TEST=value\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", QuitCommand), "QUIT\r\n");
|
||||
assert_eq!(format!("{}", DataCommand), "DATA\r\n");
|
||||
assert_eq!(format!("{}", NoopCommand), "NOOP\r\n");
|
||||
assert_eq!(format!("{}", HelpCommand::new(None)), "HELP\r\n");
|
||||
assert_eq!(
|
||||
format!("{}", HelpCommand::new(Some("test".to_string()))),
|
||||
"HELP test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", VrfyCommand::new("test".to_string())),
|
||||
"VRFY test\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", ExpnCommand::new("test".to_string())),
|
||||
"EXPN test\r\n"
|
||||
);
|
||||
assert_eq!(format!("{}", RsetCommand), "RSET\r\n");
|
||||
let credentials = Credentials::new("user".to_string(), "password".to_string());
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Plain, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH PLAIN AHVzZXIAcGFzc3dvcmQ=\r\n"
|
||||
);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{}",
|
||||
AuthCommand::new(Mechanism::Login, credentials.clone(), None).unwrap()
|
||||
),
|
||||
"AUTH LOGIN\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
132
lettre/src/smtp/error.rs
Normal file
132
lettre/src/smtp/error.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use self::Error::*;
|
||||
use base64::DecodeError;
|
||||
use native_tls;
|
||||
use nom;
|
||||
use smtp::response::{Response, Severity};
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::io;
|
||||
use std::string::FromUtf8Error;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Transient SMTP error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Transient(Response),
|
||||
/// Permanent SMTP error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
Permanent(Response),
|
||||
/// Error parsing a response
|
||||
ResponseParsing(&'static str),
|
||||
/// Error parsing a base64 string in response
|
||||
ChallengeParsing(DecodeError),
|
||||
/// Error parsing UTF8in response
|
||||
Utf8Parsing(FromUtf8Error),
|
||||
/// Internal client error
|
||||
Client(&'static str),
|
||||
/// DNS resolution error
|
||||
Resolution,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
/// TLS error
|
||||
Tls(native_tls::Error),
|
||||
/// Parsing error
|
||||
Parsing(nom::ErrorKind),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(clippy::match_same_arms))]
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
// Try to display the first line of the server's response that usually
|
||||
// contains a short humanly readable error message
|
||||
Transient(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed transient error during SMTP transaction",
|
||||
},
|
||||
Permanent(ref err) => match err.first_line() {
|
||||
Some(line) => line,
|
||||
None => "undetailed permanent error during SMTP transaction",
|
||||
},
|
||||
ResponseParsing(err) => err,
|
||||
ChallengeParsing(ref err) => err.description(),
|
||||
Utf8Parsing(ref err) => err.description(),
|
||||
Resolution => "could not resolve hostname",
|
||||
Client(err) => err,
|
||||
Io(ref err) => err.description(),
|
||||
Tls(ref err) => err.description(),
|
||||
Parsing(ref err) => err.description(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&StdError> {
|
||||
match *self {
|
||||
ChallengeParsing(ref err) => Some(&*err),
|
||||
Utf8Parsing(ref err) => Some(&*err),
|
||||
Io(ref err) => Some(&*err),
|
||||
Tls(ref err) => Some(&*err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<native_tls::Error> for Error {
|
||||
fn from(err: native_tls::Error) -> Error {
|
||||
Tls(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<nom::ErrorKind> for Error {
|
||||
fn from(err: nom::ErrorKind) -> Error {
|
||||
Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DecodeError> for Error {
|
||||
fn from(err: DecodeError) -> Error {
|
||||
ChallengeParsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FromUtf8Error> for Error {
|
||||
fn from(err: FromUtf8Error) -> Error {
|
||||
Utf8Parsing(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Response> for Error {
|
||||
fn from(response: Response) -> Error {
|
||||
match response.code.severity {
|
||||
Severity::TransientNegativeCompletion => Transient(response),
|
||||
Severity::PermanentNegativeCompletion => Permanent(response),
|
||||
_ => Client("Unknown error code"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Error {
|
||||
fn from(string: &'static str) -> Error {
|
||||
Client(string)
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, Error>;
|
||||
389
lettre/src/smtp/extension.rs
Normal file
389
lettre/src/smtp/extension.rs
Normal file
@@ -0,0 +1,389 @@
|
||||
//! ESMTP features
|
||||
|
||||
use hostname::get_hostname;
|
||||
use smtp::authentication::Mechanism;
|
||||
use smtp::error::Error;
|
||||
use smtp::response::Response;
|
||||
use smtp::util::XText;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
use std::result::Result;
|
||||
|
||||
/// Default client id
|
||||
pub const DEFAULT_DOMAIN_CLIENT_ID: &str = "localhost";
|
||||
|
||||
/// Client identifier, the parameter to `EHLO`
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum ClientId {
|
||||
/// A fully-qualified domain name
|
||||
Domain(String),
|
||||
/// An IPv4 address
|
||||
Ipv4(Ipv4Addr),
|
||||
/// An IPv6 address
|
||||
Ipv6(Ipv6Addr),
|
||||
}
|
||||
|
||||
impl Display for ClientId {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ClientId::Domain(ref value) => f.write_str(value),
|
||||
ClientId::Ipv4(ref value) => write!(f, "{}", value),
|
||||
ClientId::Ipv6(ref value) => write!(f, "{}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientId {
|
||||
/// Creates a new `ClientId` from a fully qualified domain name
|
||||
pub fn new(domain: String) -> ClientId {
|
||||
ClientId::Domain(domain)
|
||||
}
|
||||
|
||||
/// Defines a `ClientId` with the current hostname, of `localhost` if hostname could not be
|
||||
/// found
|
||||
pub fn hostname() -> ClientId {
|
||||
ClientId::Domain(get_hostname().unwrap_or_else(|| DEFAULT_DOMAIN_CLIENT_ID.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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 => write!(f, "8BITMIME"),
|
||||
Extension::SmtpUtfEight => write!(f, "SMTPUTF8"),
|
||||
Extension::StartTls => write!(f, "STARTTLS"),
|
||||
Extension::Authentication(ref mechanism) => write!(f, "AUTH {}", mechanism),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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 {
|
||||
write!(
|
||||
f,
|
||||
"{} with {}",
|
||||
self.name,
|
||||
if self.features.is_empty() {
|
||||
"no supported features".to_string()
|
||||
} else {
|
||||
format!("{:?}", self.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 split: Vec<&str> = line.split_whitespace().collect();
|
||||
match split[0] {
|
||||
"8BITMIME" => {
|
||||
features.insert(Extension::EightBitMime);
|
||||
}
|
||||
"SMTPUTF8" => {
|
||||
features.insert(Extension::SmtpUtfEight);
|
||||
}
|
||||
"STARTTLS" => {
|
||||
features.insert(Extension::StartTls);
|
||||
}
|
||||
"AUTH" => {
|
||||
for &mechanism in &split[1..] {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
/// A `MAIL FROM` extension parameter
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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-impls", derive(Serialize, 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-impls", derive(Serialize, 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::{ClientId, Extension, ServerInfo};
|
||||
use smtp::authentication::Mechanism;
|
||||
use smtp::response::{Category, Code, Detail, Response, Severity};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_clientid_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", ClientId::new("test".to_string())),
|
||||
"test".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.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(Mechanism::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,
|
||||
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));
|
||||
}
|
||||
}
|
||||
479
lettre/src/smtp/mod.rs
Normal file
479
lettre/src/smtp/mod.rs
Normal file
@@ -0,0 +1,479 @@
|
||||
//! The SMTP transport sends emails using the SMTP protocol.
|
||||
//!
|
||||
//! This SMTP client follows [RFC
|
||||
//! 5321](https://tools.ietf.org/html/rfc5321), and is designed to efficiently send emails from an
|
||||
//! application to a relay email server, as it relies as much as possible on the relay server
|
||||
//! for sanity and RFC compliance checks.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954)) with PLAIN, LOGIN and XOAUTH2 mechanisms
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
|
||||
use native_tls::TlsConnector;
|
||||
use smtp::authentication::{
|
||||
Credentials, Mechanism, DEFAULT_ENCRYPTED_MECHANISMS, DEFAULT_UNENCRYPTED_MECHANISMS,
|
||||
};
|
||||
use smtp::client::net::ClientTlsParameters;
|
||||
use smtp::client::net::DEFAULT_TLS_PROTOCOLS;
|
||||
use smtp::client::InnerClient;
|
||||
use smtp::commands::*;
|
||||
use smtp::error::{Error, SmtpResult};
|
||||
use smtp::extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo};
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::time::Duration;
|
||||
use {SendableEmail, Transport};
|
||||
|
||||
pub mod authentication;
|
||||
pub mod client;
|
||||
pub mod commands;
|
||||
pub mod error;
|
||||
pub mod extension;
|
||||
#[cfg(feature = "connection-pool")]
|
||||
pub mod r2d2;
|
||||
pub mod response;
|
||||
pub mod util;
|
||||
|
||||
// Registered port numbers:
|
||||
// https://www.iana.
|
||||
// org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub const SMTP_PORT: u16 = 25;
|
||||
/// Default submission port
|
||||
pub const SUBMISSION_PORT: u16 = 587;
|
||||
/// Default submission over TLS port
|
||||
pub const SUBMISSIONS_PORT: u16 = 465;
|
||||
|
||||
/// How to apply TLS to a client connection
|
||||
#[derive(Clone)]
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub enum ClientSecurity {
|
||||
/// Insecure connection only (for testing purposes)
|
||||
None,
|
||||
/// Start with insecure connection and use `STARTTLS` when available
|
||||
Opportunistic(ClientTlsParameters),
|
||||
/// Start with insecure connection and require `STARTTLS`
|
||||
Required(ClientTlsParameters),
|
||||
/// Use TLS wrapped connection
|
||||
Wrapper(ClientTlsParameters),
|
||||
}
|
||||
|
||||
/// Configures connection reuse behavior
|
||||
#[derive(Clone, Debug, Copy)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, Deserialize))]
|
||||
pub enum ConnectionReuseParameters {
|
||||
/// Unlimited connection reuse
|
||||
ReuseUnlimited,
|
||||
/// Maximum number of connection reuse
|
||||
ReuseLimited(u16),
|
||||
/// Disable connection reuse, close connection after each transaction
|
||||
NoReuse,
|
||||
}
|
||||
|
||||
/// Contains client configuration
|
||||
#[allow(missing_debug_implementations)]
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpClient {
|
||||
/// Enable connection reuse
|
||||
connection_reuse: ConnectionReuseParameters,
|
||||
/// Name sent during EHLO
|
||||
hello_name: ClientId,
|
||||
/// Credentials
|
||||
credentials: Option<Credentials>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
/// TLS security configuration
|
||||
security: ClientSecurity,
|
||||
/// Enable UTF8 mailboxes in envelope or headers
|
||||
smtp_utf8: bool,
|
||||
/// Optional enforced authentication mechanism
|
||||
authentication_mechanism: Option<Mechanism>,
|
||||
/// Define network timeout
|
||||
/// It can be changed later for specific needs (like a different timeout for each SMTP command)
|
||||
timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP `SmtpTransport`
|
||||
impl SmtpClient {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// Defaults are:
|
||||
///
|
||||
/// * No connection reuse
|
||||
/// * No authentication
|
||||
/// * No SMTPUTF8 support
|
||||
/// * A 60 seconds timeout for smtp commands
|
||||
///
|
||||
/// Consider using [`SmtpClient::new_simple`] instead, if possible.
|
||||
pub fn new<A: ToSocketAddrs>(addr: A, security: ClientSecurity) -> Result<SmtpClient, Error> {
|
||||
let mut addresses = addr.to_socket_addrs()?;
|
||||
|
||||
match addresses.next() {
|
||||
Some(addr) => Ok(SmtpClient {
|
||||
server_addr: addr,
|
||||
security,
|
||||
smtp_utf8: false,
|
||||
credentials: None,
|
||||
connection_reuse: ConnectionReuseParameters::NoReuse,
|
||||
hello_name: ClientId::hostname(),
|
||||
authentication_mechanism: None,
|
||||
timeout: Some(Duration::new(60, 0)),
|
||||
}),
|
||||
None => Err(Error::Resolution),
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple and secure transport, should be used when possible.
|
||||
/// Creates an encrypted transport over submissions port, using the provided domain
|
||||
/// to validate TLS certificates.
|
||||
pub fn new_simple(domain: &str) -> Result<SmtpClient, Error> {
|
||||
let mut tls_builder = TlsConnector::builder();
|
||||
tls_builder.min_protocol_version(Some(DEFAULT_TLS_PROTOCOLS[0]));
|
||||
|
||||
let tls_parameters =
|
||||
ClientTlsParameters::new(domain.to_string(), tls_builder.build().unwrap());
|
||||
|
||||
SmtpClient::new(
|
||||
(domain, SUBMISSIONS_PORT),
|
||||
ClientSecurity::Wrapper(tls_parameters),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn new_unencrypted_localhost() -> Result<SmtpClient, Error> {
|
||||
SmtpClient::new(("localhost", SMTP_PORT), ClientSecurity::None)
|
||||
}
|
||||
|
||||
/// Enable SMTPUTF8 if the server supports it
|
||||
pub fn smtp_utf8(mut self, enabled: bool) -> SmtpClient {
|
||||
self.smtp_utf8 = enabled;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the name used during EHLO
|
||||
pub fn hello_name(mut self, name: ClientId) -> SmtpClient {
|
||||
self.hello_name = name;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection reuse
|
||||
pub fn connection_reuse(mut self, parameters: ConnectionReuseParameters) -> SmtpClient {
|
||||
self.connection_reuse = parameters;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the client credentials
|
||||
pub fn credentials<S: Into<Credentials>>(mut self, credentials: S) -> SmtpClient {
|
||||
self.credentials = Some(credentials.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the authentication mechanism to use
|
||||
pub fn authentication_mechanism(mut self, mechanism: Mechanism) -> SmtpClient {
|
||||
self.authentication_mechanism = Some(mechanism);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the timeout duration
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> SmtpClient {
|
||||
self.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn transport(self) -> SmtpTransport {
|
||||
SmtpTransport::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the state of a client
|
||||
#[derive(Debug)]
|
||||
struct State {
|
||||
/// Panic state
|
||||
pub panic: bool,
|
||||
/// Connection reuse counter
|
||||
pub connection_reuse_count: u16,
|
||||
}
|
||||
|
||||
/// Structure that implements the high level SMTP client
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct SmtpTransport {
|
||||
/// Information about the server
|
||||
/// Value is None before HELO/EHLO
|
||||
server_info: Option<ServerInfo>,
|
||||
/// SmtpTransport variable states
|
||||
state: State,
|
||||
/// Information about the client
|
||||
client_info: SmtpClient,
|
||||
/// Low level client
|
||||
client: InnerClient,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.close();
|
||||
}
|
||||
return Err(From::from(err))
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl<'a> SmtpTransport {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connect to the server, but only creates the `SmtpTransport`
|
||||
pub fn new(builder: SmtpClient) -> SmtpTransport {
|
||||
let client = InnerClient::new();
|
||||
|
||||
SmtpTransport {
|
||||
client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn connect(&mut self) -> Result<(), Error> {
|
||||
// Check if the connection is still available
|
||||
if (self.state.connection_reuse_count > 0) && (!self.client.is_connected()) {
|
||||
self.close();
|
||||
}
|
||||
|
||||
if self.state.connection_reuse_count > 0 {
|
||||
info!(
|
||||
"connection already established to {}",
|
||||
self.client_info.server_addr
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.client.connect(
|
||||
&self.client_info.server_addr,
|
||||
match self.client_info.security {
|
||||
ClientSecurity::Wrapper(ref tls_parameters) => Some(tls_parameters),
|
||||
_ => None,
|
||||
},
|
||||
)?;
|
||||
|
||||
self.client.set_timeout(self.client_info.timeout)?;
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
self.ehlo()?;
|
||||
|
||||
match (
|
||||
&self.client_info.security.clone(),
|
||||
self.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::StartTls),
|
||||
) {
|
||||
(&ClientSecurity::Required(_), false) => {
|
||||
return Err(From::from("Could not encrypt connection, aborting"));
|
||||
}
|
||||
(&ClientSecurity::Opportunistic(_), false) => (),
|
||||
(&ClientSecurity::None, _) => (),
|
||||
(&ClientSecurity::Wrapper(_), _) => (),
|
||||
(&ClientSecurity::Opportunistic(ref tls_parameters), true)
|
||||
| (&ClientSecurity::Required(ref tls_parameters), true) => {
|
||||
try_smtp!(self.client.command(StarttlsCommand), self);
|
||||
try_smtp!(self.client.upgrade_tls_stream(tls_parameters), self);
|
||||
|
||||
debug!("connection encrypted");
|
||||
|
||||
// Send EHLO again
|
||||
self.ehlo()?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.client_info.credentials.is_some() {
|
||||
let mut found = false;
|
||||
|
||||
// Compute accepted mechanism
|
||||
let accepted_mechanisms = match self.client_info.authentication_mechanism {
|
||||
Some(mechanism) => vec![mechanism],
|
||||
None => {
|
||||
if self.client.is_encrypted() {
|
||||
DEFAULT_ENCRYPTED_MECHANISMS.to_vec()
|
||||
} else {
|
||||
DEFAULT_UNENCRYPTED_MECHANISMS.to_vec()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for mechanism in accepted_mechanisms {
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_auth_mechanism(mechanism)
|
||||
{
|
||||
found = true;
|
||||
try_smtp!(
|
||||
self.client
|
||||
.auth(mechanism, self.client_info.credentials.as_ref().unwrap(),),
|
||||
self
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
info!("No supported authentication mechanisms available");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets the EHLO response and updates server information
|
||||
fn ehlo(&mut self) -> SmtpResult {
|
||||
// Extended Hello
|
||||
let ehlo_response = try_smtp!(
|
||||
self.client.command(EhloCommand::new(ClientId::new(
|
||||
self.client_info.hello_name.to_string()
|
||||
),)),
|
||||
self
|
||||
);
|
||||
|
||||
self.server_info = Some(try_smtp!(ServerInfo::from_response(&ehlo_response), self));
|
||||
|
||||
// Print server information
|
||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||
|
||||
Ok(ehlo_response)
|
||||
}
|
||||
|
||||
/// Reset the client state
|
||||
pub fn close(&mut self) {
|
||||
// Close the SMTP transaction if needed
|
||||
self.client.close();
|
||||
|
||||
// Reset the client state
|
||||
self.server_info = None;
|
||||
self.state.panic = false;
|
||||
self.state.connection_reuse_count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Transport<'a> for SmtpTransport {
|
||||
type Result = SmtpResult;
|
||||
|
||||
/// Sends an email
|
||||
#[cfg_attr(
|
||||
feature = "cargo-clippy",
|
||||
allow(clippy::match_same_arms, clippy::cyclomatic_complexity)
|
||||
)]
|
||||
fn send(&mut self, email: SendableEmail) -> SmtpResult {
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
if !self.client.is_connected() {
|
||||
self.connect()?;
|
||||
}
|
||||
|
||||
// Mail
|
||||
let mut mail_options = vec![];
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::EightBitMime)
|
||||
{
|
||||
mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
|
||||
}
|
||||
|
||||
if self
|
||||
.server_info
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.supports_feature(Extension::SmtpUtfEight)
|
||||
&& self.client_info.smtp_utf8
|
||||
{
|
||||
mail_options.push(MailParameter::SmtpUtfEight);
|
||||
}
|
||||
|
||||
try_smtp!(
|
||||
self.client.command(MailCommand::new(
|
||||
email.envelope().from().cloned(),
|
||||
mail_options,
|
||||
)),
|
||||
self
|
||||
);
|
||||
|
||||
// Log the mail command
|
||||
info!(
|
||||
"{}: from=<{}>",
|
||||
message_id,
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
// Recipient
|
||||
for to_address in email.envelope().to() {
|
||||
try_smtp!(
|
||||
self.client
|
||||
.command(RcptCommand::new(to_address.clone(), vec![])),
|
||||
self
|
||||
);
|
||||
// Log the rcpt command
|
||||
info!("{}: to=<{}>", message_id, to_address);
|
||||
}
|
||||
|
||||
// Data
|
||||
try_smtp!(self.client.command(DataCommand), self);
|
||||
|
||||
// Message content
|
||||
let result = self.client.message(Box::new(email.message()));
|
||||
|
||||
if result.is_ok() {
|
||||
// Increment the connection reuse counter
|
||||
self.state.connection_reuse_count += 1;
|
||||
|
||||
// Log the message
|
||||
info!(
|
||||
"{}: conn_use={}, status=sent ({})",
|
||||
message_id,
|
||||
self.state.connection_reuse_count,
|
||||
result
|
||||
.as_ref()
|
||||
.ok()
|
||||
.unwrap()
|
||||
.message
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&"no response".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Test if we can reuse the existing connection
|
||||
match self.client_info.connection_reuse {
|
||||
ConnectionReuseParameters::ReuseLimited(limit)
|
||||
if self.state.connection_reuse_count >= limit =>
|
||||
{
|
||||
self.close()
|
||||
}
|
||||
ConnectionReuseParameters::NoReuse => self.close(),
|
||||
_ => (),
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
38
lettre/src/smtp/r2d2.rs
Normal file
38
lettre/src/smtp/r2d2.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use r2d2::ManageConnection;
|
||||
use smtp::error::Error;
|
||||
use smtp::{ConnectionReuseParameters, SmtpClient, SmtpTransport};
|
||||
|
||||
pub struct SmtpConnectionManager {
|
||||
transport_builder: SmtpClient,
|
||||
}
|
||||
|
||||
impl SmtpConnectionManager {
|
||||
pub fn new(transport_builder: SmtpClient) -> Result<SmtpConnectionManager, Error> {
|
||||
Ok(SmtpConnectionManager {
|
||||
transport_builder: transport_builder
|
||||
.connection_reuse(ConnectionReuseParameters::ReuseUnlimited),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageConnection for SmtpConnectionManager {
|
||||
type Connection = SmtpTransport;
|
||||
type Error = Error;
|
||||
|
||||
fn connect(&self) -> Result<Self::Connection, Error> {
|
||||
let mut transport = SmtpTransport::new(self.transport_builder.clone());
|
||||
transport.connect()?;
|
||||
Ok(transport)
|
||||
}
|
||||
|
||||
fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Error> {
|
||||
if conn.client.is_connected() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::Client("is not connected anymore"))
|
||||
}
|
||||
|
||||
fn has_broken(&self, conn: &mut Self::Connection) -> bool {
|
||||
conn.state.panic
|
||||
}
|
||||
}
|
||||
559
lettre/src/smtp/response.rs
Normal file
559
lettre/src/smtp/response.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
//! SMTP response, containing a mandatory return code and an optional text
|
||||
//! message
|
||||
|
||||
use nom::{crlf, ErrorKind as NomErrorKind};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
use std::result;
|
||||
use std::str::{from_utf8, FromStr};
|
||||
|
||||
/// First digit indicates severity
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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-impls", derive(Serialize, 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-impls", derive(Serialize, 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-impls", derive(Serialize, 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-impls", derive(Serialize, 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 = NomErrorKind;
|
||||
|
||||
fn from_str(s: &str) -> result::Result<Response, NomErrorKind> {
|
||||
match parse_response(s.as_bytes()) {
|
||||
Ok((_, res)) => Ok(res),
|
||||
Err(e) => Err(e.into_error_kind()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
match self.code.severity {
|
||||
Severity::PositiveCompletion | Severity::PositiveIntermediate => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
.get(0)
|
||||
.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)
|
||||
|
||||
named!(
|
||||
parse_code<Code>,
|
||||
map!(
|
||||
tuple!(parse_severity, parse_category, parse_detail),
|
||||
|(severity, category, detail)| Code {
|
||||
severity,
|
||||
category,
|
||||
detail,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
named!(
|
||||
parse_severity<Severity>,
|
||||
alt!(
|
||||
tag!("2") => { |_| Severity::PositiveCompletion } |
|
||||
tag!("3") => { |_| Severity::PositiveIntermediate } |
|
||||
tag!("4") => { |_| Severity::TransientNegativeCompletion } |
|
||||
tag!("5") => { |_| Severity::PermanentNegativeCompletion }
|
||||
)
|
||||
);
|
||||
|
||||
named!(
|
||||
parse_category<Category>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Category::Syntax } |
|
||||
tag!("1") => { |_| Category::Information } |
|
||||
tag!("2") => { |_| Category::Connections } |
|
||||
tag!("3") => { |_| Category::Unspecified3 } |
|
||||
tag!("4") => { |_| Category::Unspecified4 } |
|
||||
tag!("5") => { |_| Category::MailSystem }
|
||||
)
|
||||
);
|
||||
|
||||
named!(
|
||||
parse_detail<Detail>,
|
||||
alt!(
|
||||
tag!("0") => { |_| Detail::Zero } |
|
||||
tag!("1") => { |_| Detail::One } |
|
||||
tag!("2") => { |_| Detail::Two } |
|
||||
tag!("3") => { |_| Detail::Three } |
|
||||
tag!("4") => { |_| Detail::Four} |
|
||||
tag!("5") => { |_| Detail::Five } |
|
||||
tag!("6") => { |_| Detail::Six} |
|
||||
tag!("7") => { |_| Detail::Seven } |
|
||||
tag!("8") => { |_| Detail::Eight } |
|
||||
tag!("9") => { |_| Detail::Nine }
|
||||
)
|
||||
);
|
||||
|
||||
named!(
|
||||
parse_response<Response>,
|
||||
map_res!(
|
||||
tuple!(
|
||||
// Parse any number of continuation lines.
|
||||
many0!(tuple!(
|
||||
parse_code,
|
||||
preceded!(char!('-'), take_until_and_consume!(b"\r\n".as_ref()))
|
||||
)),
|
||||
// Parse the final line.
|
||||
tuple!(
|
||||
parse_code,
|
||||
terminated!(
|
||||
opt!(preceded!(char!(' '), take_until!(b"\r\n".as_ref()))),
|
||||
crlf
|
||||
)
|
||||
)
|
||||
),
|
||||
|(lines, (last_code, last_line)): (Vec<_>, _)| {
|
||||
// Check that all codes are equal.
|
||||
if !lines.iter().all(|&(ref code, _)| *code == last_code) {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// Extract text from lines, and append last line.
|
||||
let mut lines = lines.into_iter().map(|(_, text)| text).collect::<Vec<_>>();
|
||||
if let Some(text) = last_line {
|
||||
lines.push(text);
|
||||
}
|
||||
|
||||
Ok(Response {
|
||||
code: last_code,
|
||||
message: lines
|
||||
.into_iter()
|
||||
.map(|line| from_utf8(line).map(|s| s.to_string()))
|
||||
.collect::<result::Result<Vec<_>, _>>()
|
||||
.map_err(|_| ())?,
|
||||
})
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Category, Code, Detail, Response, Severity};
|
||||
|
||||
#[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_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("")
|
||||
);
|
||||
}
|
||||
}
|
||||
46
lettre/src/smtp/util.rs
Normal file
46
lettre/src/smtp/util.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Utils for string manipulation
|
||||
|
||||
use std::fmt::{Display, Formatter, Result as FmtResult};
|
||||
|
||||
/// Encode a string as xtext
|
||||
#[derive(Debug)]
|
||||
#[cfg_attr(feature = "serde-impls", derive(Serialize, 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 vec![
|
||||
("bjorn", "bjorn"),
|
||||
("bjørn", "bjørn"),
|
||||
("Ø+= ❤️‰", "Ø+2B+3D+20❤️‰"),
|
||||
("+", "+2B"),
|
||||
] {
|
||||
assert_eq!(format!("{}", XText(input)), expect);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
lettre/src/stub/mod.rs
Normal file
44
lettre/src/stub/mod.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
//! The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
//! testing purposes.
|
||||
//!
|
||||
|
||||
use SendableEmail;
|
||||
use Transport;
|
||||
|
||||
/// This transport logs the message envelope and returns the given response
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StubTransport {
|
||||
response: StubResult,
|
||||
}
|
||||
|
||||
impl StubTransport {
|
||||
/// Creates a new transport that always returns the given response
|
||||
pub fn new(response: StubResult) -> StubTransport {
|
||||
StubTransport { response }
|
||||
}
|
||||
|
||||
/// Creates a new transport that always returns a success response
|
||||
pub fn new_positive() -> StubTransport {
|
||||
StubTransport { response: Ok(()) }
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type StubResult = Result<(), ()>;
|
||||
|
||||
impl<'a> Transport<'a> for StubTransport {
|
||||
type Result = StubResult;
|
||||
|
||||
fn send(&mut self, email: SendableEmail) -> StubResult {
|
||||
info!(
|
||||
"{}: from=<{}> to=<{:?}>",
|
||||
email.message_id(),
|
||||
match email.envelope().from() {
|
||||
Some(address) => address.to_string(),
|
||||
None => "".to_string(),
|
||||
},
|
||||
email.envelope().to()
|
||||
);
|
||||
self.response
|
||||
}
|
||||
}
|
||||
74
lettre/tests/r2d2_smtp.rs
Normal file
74
lettre/tests/r2d2_smtp.rs
Normal file
@@ -0,0 +1,74 @@
|
||||
#[cfg(all(test, feature = "smtp-transport", feature = "connection-pool"))]
|
||||
mod test {
|
||||
extern crate lettre;
|
||||
extern crate r2d2;
|
||||
|
||||
use self::lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient};
|
||||
use self::lettre::{SmtpConnectionManager, Transport};
|
||||
use self::r2d2::Pool;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
|
||||
fn email(message: &str) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
message.to_string().into_bytes(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_one() {
|
||||
let client = SmtpClient::new("localhost:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(1).build(manager).unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
let result = (*mailer).send(email("send one"));
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_from_thread() {
|
||||
let client = SmtpClient::new("127.0.0.1:2525", ClientSecurity::None).unwrap();
|
||||
let manager = SmtpConnectionManager::new(client).unwrap();
|
||||
let pool = Pool::builder().max_size(2).build(manager).unwrap();
|
||||
|
||||
let (s1, r1) = mpsc::channel();
|
||||
let (s2, r2) = mpsc::channel();
|
||||
|
||||
let pool1 = pool.clone();
|
||||
let t1 = thread::spawn(move || {
|
||||
let mut conn = pool1.get().unwrap();
|
||||
s1.send(()).unwrap();
|
||||
r2.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 1"))
|
||||
.expect("Send failed from thread 1");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
let pool2 = pool.clone();
|
||||
let t2 = thread::spawn(move || {
|
||||
let mut conn = pool2.get().unwrap();
|
||||
s2.send(()).unwrap();
|
||||
r1.recv().unwrap();
|
||||
(*conn)
|
||||
.send(email("send from thread 2"))
|
||||
.expect("Send failed from thread 2");
|
||||
drop(conn);
|
||||
});
|
||||
|
||||
t1.join().unwrap();
|
||||
t2.join().unwrap();
|
||||
|
||||
let mut mailer = pool.get().unwrap();
|
||||
(*mailer)
|
||||
.send(email("send from main thread"))
|
||||
.expect("Send failed from main thread");
|
||||
}
|
||||
}
|
||||
65
lettre/tests/skeptic.rs
Normal file
65
lettre/tests/skeptic.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_readme() {
|
||||
let readme = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("README.md");
|
||||
|
||||
skeptic_test(&readme);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/sending-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}", path)
|
||||
);
|
||||
}
|
||||
44
lettre/tests/transport_file.rs
Normal file
44
lettre/tests/transport_file.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "file-transport")]
|
||||
mod test {
|
||||
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
use std::env::temp_dir;
|
||||
use std::fs::remove_file;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
||||
#[test]
|
||||
fn file_transport() {
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let message_id = email.message_id().to_string();
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let file = format!("{}/{}.json", temp_dir().to_str().unwrap(), message_id);
|
||||
let mut f = File::open(file.clone()).unwrap();
|
||||
let mut buffer = String::new();
|
||||
let _ = f.read_to_string(&mut buffer);
|
||||
|
||||
assert_eq!(
|
||||
buffer,
|
||||
"{\"envelope\":{\"forward_path\":[\"root@localhost\"],\"reverse_path\":\"user@localhost\"},\"message_id\":\"id\",\"message\":[72,101,108,108,111,32,195,159,226,152,186,32,101,120,97,109,112,108,101]}"
|
||||
);
|
||||
|
||||
remove_file(file).unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
27
lettre/tests/transport_sendmail.rs
Normal file
27
lettre/tests/transport_sendmail.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "sendmail-transport")]
|
||||
mod test {
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn sendmail_transport_simple() {
|
||||
let mut sender = SendmailTransport::new();
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
println!("{:?}", result);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
}
|
||||
27
lettre/tests/transport_smtp.rs
Normal file
27
lettre/tests/transport_smtp.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
extern crate lettre;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "smtp-transport")]
|
||||
mod test {
|
||||
use lettre::{ClientSecurity, EmailAddress, Envelope, SendableEmail, SmtpClient, Transport};
|
||||
|
||||
#[test]
|
||||
fn smtp_transport_simple() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
SmtpClient::new("127.0.0.1:2525", ClientSecurity::None)
|
||||
.unwrap()
|
||||
.transport()
|
||||
.send(email)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
}
|
||||
31
lettre/tests/transport_stub.rs
Normal file
31
lettre/tests/transport_stub.rs
Normal file
@@ -0,0 +1,31 @@
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{EmailAddress, Envelope, SendableEmail, Transport};
|
||||
|
||||
#[test]
|
||||
fn stub_transport() {
|
||||
let mut sender_ok = StubTransport::new_positive();
|
||||
let mut sender_ko = StubTransport::new(Err(()));
|
||||
let email_ok = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
let email_ko = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello ß☺ example".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
sender_ok.send(email_ok).unwrap();
|
||||
sender_ko.send(email_ko).unwrap_err();
|
||||
}
|
||||
1
lettre_email/CHANGELOG.md
Symbolic link
1
lettre_email/CHANGELOG.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../CHANGELOG.md
|
||||
31
lettre_email/Cargo.toml
Normal file
31
lettre_email/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
|
||||
name = "lettre_email"
|
||||
version = "0.9.2" # remember to update html_root_url
|
||||
description = "Email builder"
|
||||
readme = "README.md"
|
||||
homepage = "http://lettre.at"
|
||||
repository = "https://github.com/lettre/lettre"
|
||||
license = "MIT"
|
||||
authors = ["Alexis Mousset <contact@amousset.me>"]
|
||||
categories = ["email"]
|
||||
keywords = ["email", "mailer"]
|
||||
|
||||
[badges]
|
||||
travis-ci = { repository = "lettre/lettre_email" }
|
||||
appveyor = { repository = "lettre/lettre_email" }
|
||||
maintenance = { status = "actively-developed" }
|
||||
is-it-maintained-issue-resolution = { repository = "lettre/lettre_email" }
|
||||
is-it-maintained-open-issues = { repository = "lettre/lettre_email" }
|
||||
|
||||
[dev-dependencies]
|
||||
lettre = { version = "^0.9", path = "../lettre", features = ["smtp-transport"] }
|
||||
glob = "0.3"
|
||||
|
||||
[dependencies]
|
||||
email = "^0.0.20"
|
||||
mime = "^0.3"
|
||||
time = "^0.1"
|
||||
uuid = { version = "^0.7", features = ["v4"] }
|
||||
lettre = { version = "^0.9", path = "../lettre", default-features = false }
|
||||
base64 = "^0.10"
|
||||
1
lettre_email/LICENSE
Symbolic link
1
lettre_email/LICENSE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
1
lettre_email/README.md
Symbolic link
1
lettre_email/README.md
Symbolic link
@@ -0,0 +1 @@
|
||||
../README.md
|
||||
34
lettre_email/examples/local.rs
Normal file
34
lettre_email/examples/local.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
extern crate mime;
|
||||
|
||||
use lettre::{SmtpClient, Transport};
|
||||
use lettre_email::Email;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.text("Hello world.")
|
||||
.attachment_from_file(Path::new("Cargo.toml"), None, &mime::TEXT_PLAIN)
|
||||
.unwrap()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer = SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email.into());
|
||||
|
||||
if result.is_ok() {
|
||||
println!("Email sent");
|
||||
} else {
|
||||
println!("Could not send email: {:?}", result);
|
||||
}
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
53
lettre_email/src/error.rs
Normal file
53
lettre_email/src/error.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Error and result type for emails
|
||||
|
||||
use lettre;
|
||||
use std::io;
|
||||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt::{self, Display, Formatter},
|
||||
};
|
||||
use self::Error::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Envelope error
|
||||
Envelope(lettre::error::Error),
|
||||
/// Unparseable filename for attachment
|
||||
CannotParseFilename,
|
||||
/// IO error
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, fmt: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(&match *self {
|
||||
CannotParseFilename => "Could not parse attachment filename".to_owned(),
|
||||
Io(ref err) => err.to_string(),
|
||||
Envelope(ref err) => err.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for Error {
|
||||
fn cause(&self) -> Option<&dyn StdError> {
|
||||
match *self {
|
||||
Envelope(ref err) => Some(err),
|
||||
Io(ref err) => Some(err),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(err: io::Error) -> Error {
|
||||
Error::Io(err)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lettre::error::Error> for Error {
|
||||
fn from(err: lettre::error::Error) -> Error {
|
||||
Error::Envelope(err)
|
||||
}
|
||||
}
|
||||
601
lettre_email/src/lib.rs
Normal file
601
lettre_email/src/lib.rs
Normal file
@@ -0,0 +1,601 @@
|
||||
//! Lettre is a mailer written in Rust. lettre_email provides a simple email builder.
|
||||
//!
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/lettre_email/0.9.2")]
|
||||
#![deny(
|
||||
missing_docs,
|
||||
missing_debug_implementations,
|
||||
missing_copy_implementations,
|
||||
trivial_casts,
|
||||
trivial_numeric_casts,
|
||||
unsafe_code,
|
||||
unstable_features,
|
||||
unused_import_braces
|
||||
)]
|
||||
|
||||
extern crate base64;
|
||||
extern crate email as email_format;
|
||||
extern crate lettre;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
pub extern crate mime;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub use email_format::{Address, Header, Mailbox, MimeMessage, MimeMultipartType};
|
||||
use error::Error;
|
||||
use lettre::{error::Error as LettreError, EmailAddress, Envelope, SendableEmail};
|
||||
use mime::Mime;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use time::{now, Tm};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Builds a `MimeMessage` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct PartBuilder {
|
||||
/// Message
|
||||
message: MimeMessage,
|
||||
}
|
||||
|
||||
impl Default for PartBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a message id
|
||||
pub type MessageId = String;
|
||||
|
||||
/// Builds an `Email` structure
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Default)]
|
||||
pub struct EmailBuilder {
|
||||
/// Message
|
||||
message: PartBuilder,
|
||||
/// The recipients' addresses for the mail header
|
||||
to: Vec<Address>,
|
||||
/// The sender addresses for the mail header
|
||||
from: Vec<Address>,
|
||||
/// The Cc addresses for the mail header
|
||||
cc: Vec<Address>,
|
||||
/// The Bcc addresses for the mail header
|
||||
bcc: Vec<Address>,
|
||||
/// The Reply-To addresses for the mail header
|
||||
reply_to: Vec<Address>,
|
||||
/// The In-Reply-To ids for the mail header
|
||||
in_reply_to: Vec<MessageId>,
|
||||
/// The References ids for the mail header
|
||||
references: Vec<MessageId>,
|
||||
/// The sender address for the mail header
|
||||
sender: Option<Mailbox>,
|
||||
/// The envelope
|
||||
envelope: Option<Envelope>,
|
||||
/// Date issued
|
||||
date_issued: bool,
|
||||
}
|
||||
|
||||
/// Simple email representation
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub struct Email {
|
||||
/// Message
|
||||
message: Vec<u8>,
|
||||
/// Envelope
|
||||
envelope: Envelope,
|
||||
/// Message-ID
|
||||
message_id: Uuid,
|
||||
}
|
||||
|
||||
impl Into<SendableEmail> for Email {
|
||||
fn into(self) -> SendableEmail {
|
||||
SendableEmail::new(
|
||||
self.envelope.clone(),
|
||||
self.message_id.to_string(),
|
||||
self.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Email {
|
||||
/// TODO
|
||||
pub fn builder() -> EmailBuilder {
|
||||
EmailBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartBuilder {
|
||||
/// Creates a new empty part
|
||||
pub fn new() -> PartBuilder {
|
||||
PartBuilder {
|
||||
message: MimeMessage::new_blank_message(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> PartBuilder {
|
||||
self.message.headers.insert(header.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> PartBuilder {
|
||||
self.message.body = body.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines a `MimeMultipartType` value
|
||||
pub fn message_type(mut self, mime_type: MimeMultipartType) -> PartBuilder {
|
||||
self.message.message_type = Some(mime_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `ContentType` header with the given MIME type
|
||||
pub fn content_type(self, content_type: &Mime) -> PartBuilder {
|
||||
self.header(("Content-Type", content_type.to_string()))
|
||||
}
|
||||
|
||||
/// Adds a child part
|
||||
pub fn child(mut self, child: MimeMessage) -> PartBuilder {
|
||||
self.message.children.push(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Gets built `MimeMessage`
|
||||
pub fn build(mut self) -> MimeMessage {
|
||||
self.message.update_headers();
|
||||
self.message
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
message: PartBuilder::new(),
|
||||
to: vec![],
|
||||
from: vec![],
|
||||
cc: vec![],
|
||||
bcc: vec![],
|
||||
reply_to: vec![],
|
||||
in_reply_to: vec![],
|
||||
references: vec![],
|
||||
sender: None,
|
||||
envelope: None,
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body<S: Into<String>>(mut self, body: S) -> EmailBuilder {
|
||||
self.message = self.message.body(body);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn header<A: Into<Header>>(mut self, header: A) -> EmailBuilder {
|
||||
self.message = self.message.header(header);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and stores the sender address
|
||||
pub fn from<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.from.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and stores the recipient address
|
||||
pub fn to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and stores the recipient address
|
||||
pub fn cc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.cc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Bcc` header and stores the recipient address
|
||||
pub fn bcc<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.bcc.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.reply_to.push(Address::Mailbox(mailbox));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `In-Reply-To` header
|
||||
pub fn in_reply_to(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.in_reply_to.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `References` header
|
||||
pub fn references(mut self, message_id: MessageId) -> EmailBuilder {
|
||||
self.references.push(message_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: Into<Mailbox>>(mut self, address: A) -> EmailBuilder {
|
||||
let mailbox = address.into();
|
||||
self.sender = Some(mailbox);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject<S: Into<String>>(mut self, subject: S) -> EmailBuilder {
|
||||
self.message = self.message.header(("Subject".to_string(), subject.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: &Tm) -> EmailBuilder {
|
||||
self.message = self.message.header(("Date", Tm::rfc822z(date).to_string()));
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a file
|
||||
///
|
||||
/// If not specified, the filename will be extracted from the file path.
|
||||
pub fn attachment_from_file(
|
||||
self,
|
||||
path: &Path,
|
||||
filename: Option<&str>,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
self.attachment(
|
||||
fs::read(path)?.as_slice(),
|
||||
filename.unwrap_or(
|
||||
path.file_name()
|
||||
.and_then(|x| x.to_str())
|
||||
.ok_or(Error::CannotParseFilename)?,
|
||||
),
|
||||
content_type,
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds an attachment to the email from a vector of bytes.
|
||||
pub fn attachment(
|
||||
self,
|
||||
body: &[u8],
|
||||
filename: &str,
|
||||
content_type: &Mime,
|
||||
) -> Result<EmailBuilder, Error> {
|
||||
let encoded_body = base64::encode(&body);
|
||||
let content = PartBuilder::new()
|
||||
.body(encoded_body)
|
||||
.header((
|
||||
"Content-Disposition",
|
||||
format!("attachment; filename=\"{}\"", filename),
|
||||
))
|
||||
.header(("Content-Type", content_type.to_string()))
|
||||
.header(("Content-Transfer-Encoding", "base64"))
|
||||
.build();
|
||||
|
||||
Ok(self.message_type(MimeMultipartType::Mixed).child(content))
|
||||
}
|
||||
|
||||
/// Set the message type
|
||||
pub fn message_type(mut self, message_type: MimeMultipartType) -> EmailBuilder {
|
||||
self.message = self.message.message_type(message_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a child
|
||||
pub fn child(mut self, child: MimeMessage) -> EmailBuilder {
|
||||
self.message = self.message.child(child);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the email body to plain text content
|
||||
pub fn text<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body)
|
||||
.header((
|
||||
"Content-Type",
|
||||
mime::TEXT_PLAIN_UTF_8.to_string(),
|
||||
))
|
||||
.build();
|
||||
self.child(text)
|
||||
}
|
||||
|
||||
/// Sets the email body to HTML content
|
||||
pub fn html<S: Into<String>>(self, body: S) -> EmailBuilder {
|
||||
let html = PartBuilder::new()
|
||||
.body(body)
|
||||
.header((
|
||||
"Content-Type",
|
||||
mime::TEXT_HTML_UTF_8.to_string(),
|
||||
))
|
||||
.build();
|
||||
self.child(html)
|
||||
}
|
||||
|
||||
/// Sets the email content
|
||||
pub fn alternative<S: Into<String>, T: Into<String>>(
|
||||
self,
|
||||
body_html: S,
|
||||
body_text: T,
|
||||
) -> EmailBuilder {
|
||||
let text = PartBuilder::new()
|
||||
.body(body_text)
|
||||
.header((
|
||||
"Content-Type",
|
||||
mime::TEXT_PLAIN_UTF_8.to_string(),
|
||||
))
|
||||
.build();
|
||||
|
||||
let html = PartBuilder::new()
|
||||
.body(body_html)
|
||||
.header((
|
||||
"Content-Type",
|
||||
mime::TEXT_HTML_UTF_8.to_string(),
|
||||
))
|
||||
.build();
|
||||
|
||||
let alternate = PartBuilder::new()
|
||||
.message_type(MimeMultipartType::Alternative)
|
||||
.child(text)
|
||||
.child(html);
|
||||
|
||||
self.message_type(MimeMultipartType::Mixed)
|
||||
.child(alternate.build())
|
||||
}
|
||||
|
||||
/// Sets the envelope for manual destination control
|
||||
/// If this function is not called, the envelope will be calculated
|
||||
/// from the "to" and "cc" addresses you set.
|
||||
pub fn envelope(mut self, envelope: Envelope) -> EmailBuilder {
|
||||
self.envelope = Some(envelope);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds the Email
|
||||
pub fn build(mut self) -> Result<Email, Error> {
|
||||
// If there are multiple addresses in "From", the "Sender" is required.
|
||||
if self.from.len() >= 2 && self.sender.is_none() {
|
||||
// So, we must find something to put as Sender.
|
||||
for possible_sender in &self.from {
|
||||
// Only a mailbox can be used as sender, not Address::Group.
|
||||
if let Address::Mailbox(ref mbx) = *possible_sender {
|
||||
self.sender = Some(mbx.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Address::Group is not yet supported, so the line below will never panic.
|
||||
// If groups are supported one day, add another Error for this case
|
||||
// and return it here, if sender_header is still None at this point.
|
||||
assert!(self.sender.is_some());
|
||||
}
|
||||
// Add the sender header, if any.
|
||||
if let Some(ref v) = self.sender {
|
||||
self.message = self.message.header(("Sender", v.to_string()));
|
||||
}
|
||||
// Calculate the envelope
|
||||
let envelope = match self.envelope {
|
||||
Some(e) => e,
|
||||
None => {
|
||||
// we need to generate the envelope
|
||||
let mut to = vec![];
|
||||
// add all receivers in to_header and cc_header
|
||||
for receiver in self.to.iter().chain(self.cc.iter()).chain(self.bcc.iter()) {
|
||||
match *receiver {
|
||||
Address::Mailbox(ref m) => to.push(EmailAddress::from_str(&m.address)?),
|
||||
Address::Group(_, ref ms) => {
|
||||
for m in ms.iter() {
|
||||
to.push(EmailAddress::from_str(&m.address.clone())?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let from = Some(EmailAddress::from_str(&match self.sender {
|
||||
Some(x) => Ok(x.address.clone()), // if we have a sender_header, use it
|
||||
None => {
|
||||
// use a from header
|
||||
debug_assert!(self.from.len() <= 1); // else we'd have sender_header
|
||||
match self.from.first() {
|
||||
Some(a) => match *a {
|
||||
// if we have a from header
|
||||
Address::Mailbox(ref mailbox) => Ok(mailbox.address.clone()), // use it
|
||||
Address::Group(_, ref mailbox_list) => match mailbox_list.first() {
|
||||
// if it's an author group, use the first author
|
||||
Some(mailbox) => Ok(mailbox.address.clone()),
|
||||
// for an empty author group (the rarest of the rare cases)
|
||||
None => Err(Error::Envelope(
|
||||
LettreError::MissingFrom,
|
||||
)), // empty envelope sender
|
||||
},
|
||||
},
|
||||
// if we don't have a from header
|
||||
None => Err(Error::Envelope(
|
||||
LettreError::MissingFrom,
|
||||
)), // empty envelope sender
|
||||
}
|
||||
}
|
||||
}?)?);
|
||||
Envelope::new(from, to)?
|
||||
}
|
||||
};
|
||||
// Add the collected addresses as mailbox-list all at once.
|
||||
// The unwraps are fine because the conversions for Vec<Address> never errs.
|
||||
if !self.to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("To".into(), self.to).unwrap());
|
||||
}
|
||||
if !self.from.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), self.from).unwrap());
|
||||
} else if let Some(from) = envelope.from() {
|
||||
let from = vec![Address::new_mailbox(from.to_string())];
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("From".into(), from).unwrap());
|
||||
} else {
|
||||
Err(Error::Envelope(
|
||||
LettreError::MissingFrom,
|
||||
))?;
|
||||
}
|
||||
if !self.cc.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Cc".into(), self.cc).unwrap());
|
||||
}
|
||||
if !self.reply_to.is_empty() {
|
||||
self.message = self
|
||||
.message
|
||||
.header(Header::new_with_value("Reply-To".into(), self.reply_to).unwrap());
|
||||
}
|
||||
if !self.in_reply_to.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("In-Reply-To".into(), self.in_reply_to.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
if !self.references.is_empty() {
|
||||
self.message = self.message.header(
|
||||
Header::new_with_value("References".into(), self.references.join(" ")).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
if !self.date_issued {
|
||||
self.message = self
|
||||
.message
|
||||
.header(("Date", Tm::rfc822z(&now()).to_string()));
|
||||
}
|
||||
|
||||
self.message = self.message.header(("MIME-Version", "1.0"));
|
||||
|
||||
let message_id = Uuid::new_v4();
|
||||
|
||||
if let Ok(header) = Header::new_with_value(
|
||||
"Message-ID".to_string(),
|
||||
format!("<{}.lettre@localhost>", message_id),
|
||||
) {
|
||||
self.message = self.message.header(header)
|
||||
}
|
||||
|
||||
Ok(Email {
|
||||
message: self.message.build().as_string().into_bytes(),
|
||||
envelope,
|
||||
message_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{EmailBuilder, SendableEmail};
|
||||
use lettre::EmailAddress;
|
||||
use time::now;
|
||||
|
||||
#[test]
|
||||
fn test_multiple_from() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
let email: SendableEmail = email_builder
|
||||
.to("anna@example.com")
|
||||
.from("dieter@example.com")
|
||||
.from("joachim@example.com")
|
||||
.date(&date_now)
|
||||
.subject("Invitation")
|
||||
.body("We invite you!")
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Invitation\r\nSender: \
|
||||
<dieter@example.com>\r\nTo: <anna@example.com>\r\nFrom: \
|
||||
<dieter@example.com>, <joachim@example.com>\r\nMIME-Version: \
|
||||
1.0\r\nMessage-ID: <{}.lettre@localhost>\r\n\r\nWe invite you!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_builder() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.in_reply_to("original".to_string())
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
let id = email.message_id().to_string();
|
||||
assert_eq!(
|
||||
email.message_to_string().unwrap(),
|
||||
format!(
|
||||
"Date: {}\r\nSubject: Hello\r\nX-test: value\r\nSender: \
|
||||
<sender@localhost>\r\nTo: <user@localhost>\r\nFrom: \
|
||||
<user@localhost>\r\nCc: \"Alias\" <cc@localhost>\r\n\
|
||||
Reply-To: <reply@localhost>\r\nIn-Reply-To: original\r\n\
|
||||
MIME-Version: 1.0\r\nMessage-ID: \
|
||||
<{}.lettre@localhost>\r\n\r\nHello World!\r\n",
|
||||
date_now.rfc822z(),
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_email_sendable() {
|
||||
let email_builder = EmailBuilder::new();
|
||||
let date_now = now();
|
||||
|
||||
let email: SendableEmail = email_builder
|
||||
.to("user@localhost")
|
||||
.from("user@localhost")
|
||||
.cc(("cc@localhost", "Alias"))
|
||||
.bcc("bcc@localhost")
|
||||
.reply_to("reply@localhost")
|
||||
.sender("sender@localhost")
|
||||
.body("Hello World!")
|
||||
.date(&date_now)
|
||||
.subject("Hello")
|
||||
.header(("X-test", "value"))
|
||||
.build()
|
||||
.unwrap()
|
||||
.into();
|
||||
|
||||
assert_eq!(
|
||||
email.envelope().from().unwrap().to_string(),
|
||||
"sender@localhost".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
email.envelope().to(),
|
||||
vec![
|
||||
EmailAddress::new("user@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("cc@localhost".to_string()).unwrap(),
|
||||
EmailAddress::new("bcc@localhost".to_string()).unwrap(),
|
||||
]
|
||||
.as_slice()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
34
lettre_email/tests/build_with_envelope.rs
Normal file
34
lettre_email/tests/build_with_envelope.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
use lettre::{EmailAddress, Envelope};
|
||||
use lettre_email::EmailBuilder;
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_test() {
|
||||
let e = Envelope::new(
|
||||
Some(EmailAddress::new("from@example.org".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_with_envelope_without_from_test() {
|
||||
let e = Envelope::new(
|
||||
None,
|
||||
vec![EmailAddress::new("to@example.org".to_string()).unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
let _email = EmailBuilder::new()
|
||||
.envelope(e)
|
||||
.subject("subject")
|
||||
.text("message")
|
||||
.build()
|
||||
.unwrap_err();
|
||||
}
|
||||
49
lettre_email/tests/skeptic.rs
Normal file
49
lettre_email/tests/skeptic.rs
Normal file
@@ -0,0 +1,49 @@
|
||||
extern crate glob;
|
||||
|
||||
use self::glob::glob;
|
||||
use std::env;
|
||||
use std::env::consts::EXE_EXTENSION;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn book_test() {
|
||||
let mut book_path = env::current_dir().unwrap();
|
||||
book_path.push(
|
||||
Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("../website/content/creating-messages"),
|
||||
); // For some reasons, calling .parent() once more gives us None...
|
||||
|
||||
for md in glob(&format!("{}/*.md", book_path.to_str().unwrap())).unwrap() {
|
||||
skeptic_test(&md.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn skeptic_test(path: &Path) {
|
||||
let rustdoc = Path::new("rustdoc").with_extension(EXE_EXTENSION);
|
||||
let exe = env::current_exe().unwrap();
|
||||
let depdir = exe.parent().unwrap();
|
||||
|
||||
let mut cmd = Command::new(rustdoc);
|
||||
cmd.args(&["--verbose", "--test"])
|
||||
.arg("-L")
|
||||
.arg(&depdir)
|
||||
.arg(path);
|
||||
|
||||
let result = cmd
|
||||
.spawn()
|
||||
.expect("Failed to spawn process")
|
||||
.wait()
|
||||
.expect("Failed to run process");
|
||||
|
||||
assert!(
|
||||
result.success(),
|
||||
format!("Failed to run rustdoc tests on {:?}!", path)
|
||||
);
|
||||
}
|
||||
@@ -1,51 +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.
|
||||
|
||||
//! Provides authentication functions
|
||||
|
||||
use serialize::base64::{self, ToBase64, FromBase64};
|
||||
use serialize::hex::ToHex;
|
||||
use crypto::hmac::Hmac;
|
||||
use crypto::md5::Md5;
|
||||
use crypto::mac::Mac;
|
||||
|
||||
use NUL;
|
||||
|
||||
/// Returns a PLAIN mecanism response
|
||||
pub fn plain(username: &str, password: &str) -> String {
|
||||
format!("{}{}{}{}", NUL, username, NUL, password).as_bytes().to_base64(base64::STANDARD)
|
||||
}
|
||||
|
||||
/// Returns a CRAM-MD5 mecanism response
|
||||
pub fn cram_md5(username: &str, password: &str, encoded_challenge: &str) -> String {
|
||||
// TODO manage errors
|
||||
let challenge = encoded_challenge.from_base64().unwrap();
|
||||
|
||||
let mut hmac = Hmac::new(Md5::new(), password.as_bytes());
|
||||
hmac.input(&challenge);
|
||||
|
||||
format!("{} {}", username, hmac.result().code().to_hex()).as_bytes().to_base64(base64::STANDARD)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{plain, cram_md5};
|
||||
|
||||
#[test]
|
||||
fn test_plain() {
|
||||
assert_eq!(plain("username", "password").as_slice(), "AHVzZXJuYW1lAHBhc3N3b3Jk");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cram_md5() {
|
||||
assert_eq!(cram_md5("alice", "wonderland",
|
||||
"PDE3ODkzLjEzMjA2NzkxMjNAdGVzc2VyYWN0LnN1c2FtLmluPg==").as_slice(),
|
||||
"YWxpY2UgNjRiMmE0M2MxZjZlZDY4MDZhOTgwOTE0ZTIzZTc1ZjA=");
|
||||
}
|
||||
}
|
||||
@@ -1,26 +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.
|
||||
|
||||
//! A trait to represent a connected stream
|
||||
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::net::TcpStream;
|
||||
|
||||
/// A trait for the concept of opening a stream
|
||||
pub trait Connecter {
|
||||
/// Opens a connection to the given IP socket
|
||||
fn connect(addr: &SocketAddr) -> io::Result<Self>;
|
||||
}
|
||||
|
||||
impl Connecter for TcpStream {
|
||||
fn connect(addr: &SocketAddr) -> io::Result<TcpStream> {
|
||||
TcpStream::connect(addr)
|
||||
}
|
||||
}
|
||||
@@ -1,253 +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.
|
||||
|
||||
//! SMTP client
|
||||
|
||||
use std::string::String;
|
||||
use std::error::FromError;
|
||||
use std::net::TcpStream;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::io::{BufRead, BufStream, Read, Write};
|
||||
|
||||
use response::{Response, Severity, Category};
|
||||
use error::SmtpResult;
|
||||
use client::connecter::Connecter;
|
||||
use client::authentication::{plain, cram_md5};
|
||||
use {CRLF, MESSAGE_ENDING};
|
||||
|
||||
pub mod connecter;
|
||||
mod authentication;
|
||||
|
||||
/// 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>")
|
||||
}
|
||||
|
||||
/// Structure that implements the SMTP client
|
||||
pub struct Client<S = TcpStream> {
|
||||
/// TCP stream between client and server
|
||||
/// Value is None before connection
|
||||
stream: Option<BufStream<S>>,
|
||||
/// Socket we are connecting to
|
||||
server_addr: SocketAddr,
|
||||
|
||||
}
|
||||
|
||||
macro_rules! return_err (
|
||||
($err: expr, $client: ident) => ({
|
||||
return Err(FromError::from_error($err))
|
||||
})
|
||||
);
|
||||
|
||||
impl<S = TcpStream> Client<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Client`
|
||||
pub fn new<A: ToSocketAddrs>(addr: A) -> Client<S> {
|
||||
Client{
|
||||
stream: None,
|
||||
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connecter + Write + Read = TcpStream> Client<S> {
|
||||
/// Closes the SMTP transaction if possible
|
||||
pub fn close(&mut self) {
|
||||
let _ = self.quit();
|
||||
}
|
||||
|
||||
/// Connects to the configured server
|
||||
pub fn connect(&mut self) -> 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);
|
||||
}
|
||||
|
||||
// Try to connect
|
||||
self.stream = Some(BufStream::new(try!(Connecter::connect(&self.server_addr))));
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Send a HELO command and fills `server_info`
|
||||
pub fn helo(&mut self, hostname: &str) -> SmtpResult {
|
||||
self.command(&format!("HELO {}", hostname))
|
||||
}
|
||||
|
||||
/// Sends a EHLO command and fills `server_info`
|
||||
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 PLAIN mecanism
|
||||
pub fn auth_plain(&mut self, username: &str, password: &str) -> SmtpResult {
|
||||
self.command(&format!("AUTH PLAIN {}", plain(username, password)))
|
||||
}
|
||||
|
||||
/// Sends an AUTH command with CRAM-MD5 mecanism
|
||||
pub fn auth_cram_md5(&mut self, username: &str, password: &str) -> SmtpResult {
|
||||
let encoded_challenge = try!(self.command("AUTH CRAM-MD5")).first_word().expect("No challenge");
|
||||
self.command(&format!("AUTH CRAM-MD5 {}", cram_md5(username, password, &encoded_challenge)))
|
||||
}
|
||||
|
||||
/// Sends the message content and close
|
||||
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(FromError::from_error("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 line = String::new();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
|
||||
// If the string is too short to be a response code
|
||||
if line.len() < 3 {
|
||||
return Err(FromError::from_error("Could not parse reply code, line too short"));
|
||||
}
|
||||
|
||||
let (severity, category, detail) = match (line[0..1].parse::<Severity>(), line[1..2].parse::<Category>(), line[2..3].parse::<u8>()) {
|
||||
(Ok(severity), Ok(category), Ok(detail)) => (severity, category, detail),
|
||||
_ => return Err(FromError::from_error("Could not parse reply code")),
|
||||
};
|
||||
|
||||
let mut message = Vec::new();
|
||||
|
||||
// 3 chars for code + space + CRLF
|
||||
while line.len() > 6 {
|
||||
let end = line.len() - 2;
|
||||
message.push(line[4..end].to_string());
|
||||
if line.as_bytes()[3] == '-' as u8 {
|
||||
line.clear();
|
||||
try!(self.stream.as_mut().unwrap().read_line(&mut line));
|
||||
} else {
|
||||
line.clear();
|
||||
}
|
||||
}
|
||||
|
||||
let response = Response::new(severity, category, detail, message);
|
||||
|
||||
match response.is_positive() {
|
||||
true => Ok(response),
|
||||
false => Err(FromError::from_error(response)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{escape_dot, escape_crlf};
|
||||
|
||||
#[test]
|
||||
fn test_escape_dot() {
|
||||
assert_eq!(escape_dot(".test").as_slice(), "..test");
|
||||
assert_eq!(escape_dot("\r.\n.\r\n").as_slice(), "\r..\n..\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.test\r\n").as_slice(), "test\r\n..test\r\n");
|
||||
assert_eq!(escape_dot("test\r\n.\r\ntest").as_slice(), "test\r\n..\r\ntest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_crlf() {
|
||||
assert_eq!(escape_crlf("\r\n").as_slice(), "<CR><LF>");
|
||||
assert_eq!(escape_crlf("EHLO my_name\r\n").as_slice(), "EHLO my_name<CR><LF>");
|
||||
assert_eq!(
|
||||
escape_crlf("EHLO my_name\r\nSIZE 42\r\n").as_slice(),
|
||||
"EHLO my_name<CR><LF>SIZE 42<CR><LF>"
|
||||
);
|
||||
}
|
||||
}
|
||||
90
src/error.rs
90
src/error.rs
@@ -1,90 +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.
|
||||
|
||||
//! Error and result type for SMTP clients
|
||||
|
||||
use std::error::Error;
|
||||
use std::io;
|
||||
use std::error::FromError;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fmt;
|
||||
|
||||
use response::{Severity, Response};
|
||||
use self::SmtpError::*;
|
||||
|
||||
/// An enum of all error kinds.
|
||||
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||
pub enum SmtpError {
|
||||
/// Transient error, 4xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
TransientError(Response),
|
||||
/// Permanent error, 5xx reply code
|
||||
///
|
||||
/// [RFC 5321, section 4.2.1](https://tools.ietf.org/html/rfc5321#section-4.2.1)
|
||||
PermanentError(Response),
|
||||
/// TODO
|
||||
ClientError(String),
|
||||
/// IO error
|
||||
IoError(io::Error),
|
||||
}
|
||||
|
||||
impl Display for SmtpError {
|
||||
fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> {
|
||||
fmt.write_str(self.description())
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for SmtpError {
|
||||
fn description(&self) -> &str {
|
||||
match *self {
|
||||
TransientError(_) => "a transient error occured during the SMTP transaction",
|
||||
PermanentError(_) => "a permanent error occured during the SMTP transaction",
|
||||
ClientError(_) => "an unknown error occured",
|
||||
IoError(_) => "an I/O error occured",
|
||||
}
|
||||
}
|
||||
|
||||
fn cause(&self) -> Option<&Error> {
|
||||
match *self {
|
||||
IoError(ref err) => Some(&*err as &Error),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromError<io::Error> for SmtpError {
|
||||
fn from_error(err: io::Error) -> SmtpError {
|
||||
IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromError<Response> for SmtpError {
|
||||
fn from_error(response: Response) -> SmtpError {
|
||||
match response.severity() {
|
||||
Severity::TransientNegativeCompletion => TransientError(response),
|
||||
Severity::PermanentNegativeCompletion => PermanentError(response),
|
||||
_ => ClientError("Unknown error code".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromError<&'static str> for SmtpError {
|
||||
fn from_error(string: &'static str) -> SmtpError {
|
||||
ClientError(string.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// SMTP result type
|
||||
pub type SmtpResult = Result<Response, SmtpError>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// TODO
|
||||
}
|
||||
108
src/extension.rs
108
src/extension.rs
@@ -1,108 +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.
|
||||
|
||||
//! ESMTP features
|
||||
|
||||
use std::str::FromStr;
|
||||
use std::result::Result;
|
||||
|
||||
use response::Response;
|
||||
use self::Extension::*;
|
||||
|
||||
/// Supported ESMTP keywords
|
||||
#[derive(PartialEq,Eq,Copy,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 PLAIN mecanism
|
||||
///
|
||||
/// RFC 4616: https://tools.ietf.org/html/rfc4616
|
||||
PlainAuthentication,
|
||||
/// AUTH CRAM-MD5 mecanism
|
||||
///
|
||||
/// RFC 2195: https://tools.ietf.org/html/rfc2195
|
||||
CramMd5Authentication,
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
fn from_str(s: &str) -> Result<Vec<Extension>, &'static str> {
|
||||
let splitted : Vec<&str> = s.split(' ').collect();
|
||||
match (splitted[0], splitted.len()) {
|
||||
("8BITMIME", 1) => Ok(vec![EightBitMime]),
|
||||
("SMTPUTF8", 1) => Ok(vec![SmtpUtfEight]),
|
||||
("STARTTLS", 1) => Ok(vec![StartTls]),
|
||||
("AUTH", _) => {
|
||||
let mut mecanisms: Vec<Extension> = vec![];
|
||||
for &mecanism in &splitted[1..] {
|
||||
match mecanism {
|
||||
"PLAIN" => mecanisms.push(PlainAuthentication),
|
||||
"CRAM-MD5" => mecanisms.push(CramMd5Authentication),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(mecanisms)
|
||||
},
|
||||
_ => Err("Unknown extension"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses supported ESMTP features
|
||||
pub fn parse_esmtp_response(response: &Response) -> Vec<Extension> {
|
||||
let mut esmtp_features: Vec<Extension> = Vec::new();
|
||||
|
||||
for line in response.message() {
|
||||
if let Ok(keywords) = Extension::from_str(&line) {
|
||||
esmtp_features.push_all(&keywords);
|
||||
};
|
||||
}
|
||||
|
||||
esmtp_features
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Extension;
|
||||
use response::{Severity, Category, Response};
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(Extension::from_str("8BITMIME"), Ok(vec![Extension::EightBitMime]));
|
||||
assert_eq!(Extension::from_str("AUTH PLAIN"), Ok(vec![Extension::PlainAuthentication]));
|
||||
assert_eq!(Extension::from_str("AUTH PLAIN LOGIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
|
||||
assert_eq!(Extension::from_str("AUTH CRAM-MD5 PLAIN"), Ok(vec![Extension::CramMd5Authentication, Extension::PlainAuthentication]));
|
||||
assert_eq!(Extension::from_str("AUTH DIGEST-MD5 PLAIN CRAM-MD5"), Ok(vec![Extension::PlainAuthentication, Extension::CramMd5Authentication]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_esmtp_response() {
|
||||
assert_eq!(Extension::parse_esmtp_response(&Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"2".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
)), vec![Extension::EightBitMime]);
|
||||
assert_eq!(Extension::parse_esmtp_response(&Response::new(
|
||||
"4".parse::<Severity>().unwrap(),
|
||||
"3".parse::<Category>().unwrap(),
|
||||
3,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "AUTH PLAIN CRAM-MD5".to_string()]
|
||||
)), vec![Extension::EightBitMime, Extension::PlainAuthentication, Extension::CramMd5Authentication]);
|
||||
}
|
||||
}
|
||||
187
src/lib.rs
187
src/lib.rs
@@ -1,187 +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.
|
||||
|
||||
//! # Rust SMTP client
|
||||
//!
|
||||
//! 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 a rust application to a
|
||||
//! relay email server.
|
||||
//!
|
||||
//! It implements the following extensions:
|
||||
//!
|
||||
//! * 8BITMIME ([RFC 6152](https://tools.ietf.org/html/rfc6152))
|
||||
//! * AUTH ([RFC 4954](http://tools.ietf.org/html/rfc4954))
|
||||
//!
|
||||
//! It will eventually implement the following extensions:
|
||||
//!
|
||||
//! * STARTTLS ([RFC 2487](http://tools.ietf.org/html/rfc2487))
|
||||
//! * SMTPUTF8 ([RFC 6531](http://tools.ietf.org/html/rfc6531))
|
||||
//!
|
||||
//! ## Architecture
|
||||
//!
|
||||
//! This client is divided into three parts:
|
||||
//!
|
||||
//! * client: a low level SMTP client providing all SMTP commands
|
||||
//! * sender: a high level SMTP client providing an easy method to send emails
|
||||
//! * mailer: 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::mailer::EmailBuilder;
|
||||
//! use std::net::TcpStream;
|
||||
//!
|
||||
//! // 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: Sender<TcpStream> = SenderBuilder::localhost().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::mailer::EmailBuilder;
|
||||
//! use std::net::TcpStream;
|
||||
//!
|
||||
//! 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: Sender<TcpStream> = SenderBuilder::new(("server.tld", 10025))
|
||||
//! // Set the name sent during EHLO/HELO, default is `localhost`
|
||||
//! .hello_name("my.hostname.tld")
|
||||
//! // Add credentials for authentication
|
||||
//! .credentials("username", "password")
|
||||
//! // 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::sendable_email::SimpleSendableEmail;
|
||||
//! use std::net::TcpStream;
|
||||
//!
|
||||
//! // Create a minimal email
|
||||
//! let email = SimpleSendableEmail::new(
|
||||
//! "test@example.com",
|
||||
//! "test@example.org",
|
||||
//! "Hello world !"
|
||||
//! );
|
||||
//!
|
||||
//! let mut sender: Sender<TcpStream> = SenderBuilder::localhost().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 std::net::TcpStream;
|
||||
//!
|
||||
//! let mut email_client: Client<TcpStream> = Client::new(("localhost", SMTP_PORT));
|
||||
//! let _ = email_client.connect();
|
||||
//! 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();
|
||||
//! ```
|
||||
|
||||
#![feature(plugin, core, collections, str_words)]
|
||||
#![deny(missing_docs)]
|
||||
|
||||
#[macro_use] extern crate log;
|
||||
extern crate "rustc-serialize" as serialize;
|
||||
extern crate crypto;
|
||||
extern crate time;
|
||||
extern crate uuid;
|
||||
|
||||
mod extension;
|
||||
pub mod client;
|
||||
pub mod sender;
|
||||
pub mod response;
|
||||
pub mod error;
|
||||
pub mod sendable_email;
|
||||
pub mod mailer;
|
||||
|
||||
// Registrated port numbers:
|
||||
// https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
|
||||
|
||||
/// Default smtp port
|
||||
pub static SMTP_PORT: u16 = 25;
|
||||
|
||||
/// Default smtps port
|
||||
pub static SMTPS_PORT: u16 = 465;
|
||||
|
||||
/// Default submission port
|
||||
pub static SUBMISSION_PORT: u16 = 587;
|
||||
|
||||
// Useful strings and characters
|
||||
|
||||
/// The word separator for SMTP transactions
|
||||
pub static SP: &'static str = " ";
|
||||
|
||||
/// The line ending for SMTP transactions (carriage return + line feed)
|
||||
pub static CRLF: &'static str = "\r\n";
|
||||
|
||||
/// Colon
|
||||
pub static COLON: &'static str = ":";
|
||||
|
||||
/// The ending of message content
|
||||
pub static MESSAGE_ENDING: &'static str = "\r\n.\r\n";
|
||||
|
||||
/// NUL unicode character
|
||||
pub static NUL: &'static str = "\0";
|
||||
@@ -1,104 +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.
|
||||
|
||||
//! Simple SMTP "address" (very incomplete)
|
||||
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
use SP;
|
||||
|
||||
/// Converts an adress or an address with an alias to an `Address`
|
||||
pub trait ToAddress {
|
||||
/// Converts to an `Address` struct
|
||||
fn to_address(&self) -> Address;
|
||||
}
|
||||
|
||||
impl ToAddress for Address {
|
||||
fn to_address(&self) -> Address {
|
||||
(*self).clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToAddress for &'a str {
|
||||
fn to_address(&self) -> Address {
|
||||
Address::new(*self, None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToAddress for (&'a str, &'a str) {
|
||||
fn to_address(&self) -> Address {
|
||||
let (address, alias) = *self;
|
||||
Address::new(address, Some(alias))
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains an address with an optionnal alias
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub struct Address {
|
||||
/// The address
|
||||
address: String,
|
||||
/// The alias
|
||||
alias: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Address {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write! (f, "{}", match self.alias {
|
||||
Some(ref alias_string) => format!("{}{}<{}>", alias_string, SP, &self.address),
|
||||
None => self.address.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Address {
|
||||
/// Creates an address
|
||||
pub fn new(address: &str, alias: Option<&str>) -> Address {
|
||||
Address {
|
||||
address: address.to_string(),
|
||||
alias: match alias {
|
||||
Some(ref alias_string) => Some(alias_string.to_string()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Return only the address
|
||||
pub fn get_address(&self) -> String {
|
||||
self.address.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Address;
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
assert_eq!(
|
||||
Address::new("address", Some("alias")),
|
||||
Address{address: "address".to_string(), alias: Some("alias".to_string())}
|
||||
);
|
||||
assert_eq!(
|
||||
Address::new("address", None),
|
||||
Address{address: "address".to_string(), alias: None}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Address::new("address", None)),
|
||||
"address".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Address::new("address", Some("alias"))),
|
||||
"alias <address>".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +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.
|
||||
|
||||
//! Simple SMTP headers
|
||||
|
||||
use time::Tm;
|
||||
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
use mailer::address::Address;
|
||||
use {COLON, SP};
|
||||
|
||||
/// Converts to an `Header`
|
||||
pub trait ToHeader {
|
||||
/// Converts to an `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, value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains a header
|
||||
#[derive(PartialEq,Eq,Clone,Debug)]
|
||||
pub enum Header {
|
||||
/// `To`
|
||||
To(Address),
|
||||
/// `From`
|
||||
From(Address),
|
||||
/// `Cc`
|
||||
Cc(Address),
|
||||
/// `Reply-To`
|
||||
ReplyTo(Address),
|
||||
/// `Sender`
|
||||
Sender(Address),
|
||||
/// `Date`
|
||||
Date(Tm),
|
||||
/// `Subject`
|
||||
Subject(String),
|
||||
/// `MIME-Version`
|
||||
MimeVersion,
|
||||
/// `Content-Type`
|
||||
ContentType(String),
|
||||
/// `Message-Id`
|
||||
MessageId(String),
|
||||
/// Any header (name, value)
|
||||
Other(String, String),
|
||||
}
|
||||
|
||||
impl Display for Header {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
write! (f, "{}{}{}{}",
|
||||
match *self {
|
||||
Header::To(_) => "To",
|
||||
Header::From(_) => "From",
|
||||
Header::Cc(_) => "Cc",
|
||||
Header::ReplyTo(_) => "Reply-To",
|
||||
Header::Sender(_) => "Sender",
|
||||
Header::Date(_) => "Date",
|
||||
Header::Subject(_) => "Subject",
|
||||
Header::MimeVersion => "MIME-Version",
|
||||
Header::ContentType(_) => "Content-Type",
|
||||
Header::MessageId(_) => "Message-Id",
|
||||
Header::Other(ref name, _) => name.as_slice(),
|
||||
},
|
||||
COLON, SP,
|
||||
match *self {
|
||||
Header::To(ref address) => format! ("{}", address),
|
||||
Header::From(ref address) => format! ("{}", address),
|
||||
Header::Cc(ref address) => format! ("{}", address),
|
||||
Header::ReplyTo(ref address) => format! ("{}", address),
|
||||
Header::Sender(ref address) => format! ("{}", address),
|
||||
Header::Date(ref date) => Tm::rfc822(date).to_string(),
|
||||
Header::Subject(ref subject) => subject.clone(),
|
||||
Header::MimeVersion => "1.0".to_string(),
|
||||
Header::ContentType(ref string) => string.clone(),
|
||||
Header::MessageId(ref string) => string.clone(),
|
||||
Header::Other(_, ref value) => value.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Header {
|
||||
/// Creates ah `Header`
|
||||
pub fn new(name: &str, value: &str) -> Header {
|
||||
Header::Other(name.to_string(), value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Header;
|
||||
use mailer::address::Address;
|
||||
|
||||
use time::{at_utc, Timespec};
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
assert_eq!(
|
||||
Header::new("From", "me"),
|
||||
Header::Other("From".to_string(), "me".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", Header::new("From", "me")),
|
||||
"From: me".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Header::To(Address::new("me@example.com", Some("My Name")))),
|
||||
"To: My Name <me@example.com>".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", Header::Subject("Test subject".to_string())),
|
||||
"Subject: Test subject".to_string()
|
||||
);
|
||||
let time = at_utc(Timespec::new(1234567890, 54321));
|
||||
assert_eq!(
|
||||
format!("{}", Header::Date(time)),
|
||||
"Date: Fri, 13 Feb 2009 23:31:30 GMT".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,255 +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.
|
||||
|
||||
//! Simple email (very incomplete)
|
||||
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
use time::{now, Tm};
|
||||
|
||||
use mailer::header::{ToHeader, Header};
|
||||
use mailer::address::ToAddress;
|
||||
use sendable_email::SendableEmail;
|
||||
use CRLF;
|
||||
|
||||
pub mod header;
|
||||
pub mod address;
|
||||
|
||||
/// TODO
|
||||
#[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 {
|
||||
/// Array of headers
|
||||
headers: Vec<Header>,
|
||||
/// Message body
|
||||
body: String,
|
||||
/// The enveloppe recipients addresses
|
||||
to: Vec<String>,
|
||||
/// The enveloppe sender address
|
||||
from: Option<String>,
|
||||
}
|
||||
|
||||
impl Display for Email {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
let mut formatted_headers = String::new();
|
||||
for header in self.headers.iter() {
|
||||
formatted_headers.push_str(&format! ("{}", header));
|
||||
formatted_headers.push_str(CRLF);
|
||||
}
|
||||
write! (f, "{}{}{}", formatted_headers, CRLF, self.body)
|
||||
}
|
||||
}
|
||||
|
||||
impl EmailBuilder {
|
||||
/// Creates a new empty email
|
||||
pub fn new() -> EmailBuilder {
|
||||
EmailBuilder {
|
||||
content: Email {
|
||||
headers: vec![],
|
||||
body: "".to_string(),
|
||||
to: vec![],
|
||||
from: None,
|
||||
},
|
||||
date_issued: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the email body
|
||||
pub fn body(mut self, body: &str) -> EmailBuilder {
|
||||
self.content.body = body.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a generic header
|
||||
pub fn add_header<A: ToHeader>(mut self, header: A) -> EmailBuilder {
|
||||
self.content.headers.push(header.to_header());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `From` header and store the sender address
|
||||
pub fn from<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
||||
self.content.from = Some(address.to_address().get_address());
|
||||
self.content.headers.push(
|
||||
Header::From(address.to_address())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `To` header and store the recipient address
|
||||
pub fn to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
||||
self.content.to.push(address.to_address().get_address());
|
||||
self.content.headers.push(
|
||||
Header::To(address.to_address())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Cc` header and store the recipient address
|
||||
pub fn cc<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
||||
self.content.to.push(address.to_address().get_address());
|
||||
self.content.headers.push(
|
||||
Header::Cc(address.to_address())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Reply-To` header
|
||||
pub fn reply_to<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
||||
self.content.headers.push(
|
||||
Header::ReplyTo(address.to_address())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Sender` header
|
||||
pub fn sender<A: ToAddress>(mut self, address: A) -> EmailBuilder {
|
||||
self.content.from = Some(address.to_address().get_address());
|
||||
self.content.headers.push(
|
||||
Header::Sender(address.to_address())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Subject` header
|
||||
pub fn subject(mut self, subject: &str) -> EmailBuilder {
|
||||
self.content.headers.push(
|
||||
Header::Subject(subject.to_string())
|
||||
);
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a `Date` header with the given date
|
||||
pub fn date(mut self, date: Tm) -> EmailBuilder {
|
||||
self.content.headers.push(
|
||||
Header::Date(date)
|
||||
);
|
||||
self.date_issued = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the Email
|
||||
pub fn build(mut self) -> Email {
|
||||
if !self.date_issued {
|
||||
self.content.headers.push(
|
||||
Header::Date(now())
|
||||
);
|
||||
}
|
||||
self.content
|
||||
}
|
||||
}
|
||||
|
||||
impl SendableEmail for Email {
|
||||
/// Return the to addresses, and fails if it is not set
|
||||
fn to_addresses(&self) -> Vec<String> {
|
||||
if self.to.is_empty() {
|
||||
panic!("The To field is empty")
|
||||
}
|
||||
self.to.clone()
|
||||
}
|
||||
|
||||
/// Return the from address, and fails if it is not set
|
||||
fn from_address(&self) -> String {
|
||||
match self.from {
|
||||
Some(ref from_address) => from_address.clone(),
|
||||
None => panic!("The From field is empty"),
|
||||
}
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
format! ("{}", self)
|
||||
}
|
||||
|
||||
/// Adds a `Message-ID` header
|
||||
fn set_message_id(&mut self, string: String) {
|
||||
self.headers.push(
|
||||
Header::MessageId(string)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Email, EmailBuilder};
|
||||
use mailer::header::Header;
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
assert_eq!(
|
||||
EmailBuilder::new(),
|
||||
EmailBuilder{content: Email{headers: vec![], body: "".to_string(), to: vec![], from: None}, date_issued: false}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_body() {
|
||||
let email = EmailBuilder::new().body("test message");
|
||||
assert_eq!(
|
||||
email,
|
||||
EmailBuilder{content: Email {headers: vec![], body: "test message".to_string(), to: vec![], from: None}, date_issued: false}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_header() {
|
||||
let mut email = EmailBuilder::new()
|
||||
.add_header(("X-My-Header", "value"));
|
||||
assert_eq!(
|
||||
email,
|
||||
EmailBuilder{
|
||||
content: Email {
|
||||
headers: vec![Header::new("X-My-Header", "value")],
|
||||
body: "".to_string(),
|
||||
to: vec![],
|
||||
from: None
|
||||
},
|
||||
date_issued: false,
|
||||
}
|
||||
);
|
||||
email = email.add_header(("X-My-Header-2", "value-2"));
|
||||
assert_eq!(
|
||||
email,
|
||||
EmailBuilder{
|
||||
content: Email {
|
||||
headers: vec![Header::new("X-My-Header", "value"),
|
||||
Header::new("X-My-Header-2", "value-2")],
|
||||
body: "".to_string(),
|
||||
to: vec![],
|
||||
from: None
|
||||
},
|
||||
date_issued: false,
|
||||
}
|
||||
);
|
||||
email = email.add_header(("X-My-Header-3", "value-3")).add_header(("X-My-Header-4", "value-4"));
|
||||
assert_eq!(
|
||||
email,
|
||||
EmailBuilder{
|
||||
content: Email {
|
||||
headers: vec![Header::new("X-My-Header", "value"),
|
||||
Header::new("X-My-Header-2", "value-2"),
|
||||
Header::new("X-My-Header-3", "value-3"),
|
||||
Header::new("X-My-Header-4", "value-4")],
|
||||
body: "".to_string(),
|
||||
to: vec![],
|
||||
from: None
|
||||
},
|
||||
date_issued: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// TODO test Email
|
||||
}
|
||||
344
src/response.rs
344
src/response.rs
@@ -1,344 +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.
|
||||
|
||||
//! 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::Result as RResult;
|
||||
|
||||
use self::Severity::*;
|
||||
use self::Category::*;
|
||||
|
||||
/// 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 = &'static str;
|
||||
fn from_str(s: &str) -> RResult<Severity, &'static str> {
|
||||
match s {
|
||||
"2" => Ok(PositiveCompletion),
|
||||
"3" => Ok(PositiveIntermediate),
|
||||
"4" => Ok(TransientNegativeCompletion),
|
||||
"5" => Ok(PermanentNegativeCompletion),
|
||||
_ => Err("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 = &'static str;
|
||||
fn from_str(s: &str) -> RResult<Category, &'static str> {
|
||||
match s {
|
||||
"0" => Ok(Syntax),
|
||||
"1" => Ok(Information),
|
||||
"2" => Ok(Connections),
|
||||
"3" => Ok(Unspecified3),
|
||||
"4" => Ok(Unspecified4),
|
||||
"5" => Ok(MailSystem),
|
||||
_ => Err("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,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// First digit of the response code
|
||||
severity: Severity,
|
||||
/// Second digit of the response code
|
||||
category: Category,
|
||||
/// Third digit
|
||||
detail: u8,
|
||||
/// Server response string (optional)
|
||||
/// Handle multiline responses
|
||||
message: Vec<String>
|
||||
}
|
||||
|
||||
impl Display for Response {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result {
|
||||
let code = self.code();
|
||||
for line in self.message[..-1].iter() {
|
||||
let _ = write!(f, "{}-{}",
|
||||
code,
|
||||
line
|
||||
);
|
||||
}
|
||||
write!(f, "{} {}",
|
||||
code,
|
||||
self.message[-1]
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
/// Creates a new `Response`
|
||||
pub fn new(severity: Severity, category: Category, detail: u8, message: Vec<String>) -> Response {
|
||||
Response {
|
||||
severity: severity,
|
||||
category: category,
|
||||
detail: detail,
|
||||
message: message
|
||||
}
|
||||
}
|
||||
|
||||
/// Tells if the response is positive
|
||||
pub fn is_positive(&self) -> bool {
|
||||
match self.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.severity
|
||||
}
|
||||
|
||||
/// Returns the category (i.e. 2nd digit)
|
||||
pub fn category(&self) -> Category {
|
||||
self.category
|
||||
}
|
||||
|
||||
/// Returns the detail (i.e. 3rd digit)
|
||||
pub fn detail(&self) -> u8 {
|
||||
self.detail
|
||||
}
|
||||
|
||||
/// Returns the reply code
|
||||
fn code(&self) -> String {
|
||||
format!("{}{}{}", self.severity, self.category, self.detail)
|
||||
}
|
||||
|
||||
/// Checls 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 => Some(self.message[0].words().next().unwrap().to_string())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{Severity, Category, Response};
|
||||
|
||||
#[test]
|
||||
fn test_severity_from_str() {
|
||||
assert_eq!("2".parse::<Severity>(), Ok(Severity::PositiveCompletion));
|
||||
assert_eq!("4".parse::<Severity>(), Ok(Severity::TransientNegativeCompletion));
|
||||
assert!("1".parse::<Severity>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_severity_fmt() {
|
||||
assert_eq!(format!("{}", Severity::PositiveCompletion).as_slice(), "2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_from_str() {
|
||||
assert_eq!("2".parse::<Category>(), Ok(Category::Connections));
|
||||
assert_eq!("4".parse::<Category>(), Ok(Category::Unspecified4));
|
||||
assert!("6".parse::<Category>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_category_fmt() {
|
||||
assert_eq!(format!("{}", Category::Unspecified4).as_slice(), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_new() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
), Response {
|
||||
severity: Severity::PositiveCompletion,
|
||||
category: Category::Unspecified4,
|
||||
detail: 1,
|
||||
message: vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()],
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_is_positive() {
|
||||
assert!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).is_positive());
|
||||
assert!(! Response::new(
|
||||
"4".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).is_positive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_message() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).message(), vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_severity() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).severity(), Severity::PositiveCompletion);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_category() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).category(), Category::Unspecified4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_detail() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).detail(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_code() {
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).code(), "241");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_response_has_code() {
|
||||
assert!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).has_code(241));
|
||||
assert!(! Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
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(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).first_word(), Some("me".to_string()));
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec!["me mo".to_string(), "8BITMIME".to_string(), "SIZE 42".to_string()]
|
||||
).first_word(), Some("me".to_string()));
|
||||
assert_eq!(Response::new(
|
||||
"2".parse::<Severity>().unwrap(),
|
||||
"4".parse::<Category>().unwrap(),
|
||||
1,
|
||||
vec![]
|
||||
).first_word(), None);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +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.
|
||||
|
||||
//! SMTP sendable email
|
||||
|
||||
/// Email sendable by an SMTP client
|
||||
pub trait SendableEmail {
|
||||
/// From address
|
||||
fn from_address(&self) -> String;
|
||||
/// To addresses
|
||||
fn to_addresses(&self) -> Vec<String>;
|
||||
/// Message content
|
||||
fn message(&self) -> String;
|
||||
/// Set message-ID header
|
||||
fn set_message_id(&mut self, id: 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) -> String {
|
||||
self.from.clone()
|
||||
}
|
||||
|
||||
fn to_addresses(&self) -> Vec<String> {
|
||||
self.to.clone()
|
||||
}
|
||||
|
||||
fn message(&self) -> String {
|
||||
self.message.clone()
|
||||
}
|
||||
|
||||
fn set_message_id(&mut self, id: String) {
|
||||
let _ = id;
|
||||
}
|
||||
}
|
||||
@@ -1,283 +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.
|
||||
|
||||
//! Sends an email using the client
|
||||
|
||||
use std::string::String;
|
||||
use std::net::TcpStream;
|
||||
use std::net::{SocketAddr, ToSocketAddrs};
|
||||
use std::io::{Read, Write};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
use SMTP_PORT;
|
||||
use extension::Extension;
|
||||
use error::{SmtpResult, SmtpError};
|
||||
use sendable_email::SendableEmail;
|
||||
use sender::server_info::ServerInfo;
|
||||
use client::Client;
|
||||
use client::connecter::Connecter;
|
||||
|
||||
mod server_info;
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Builder for the SMTP Sender
|
||||
impl SenderBuilder {
|
||||
/// Creates a new local SMTP client
|
||||
pub fn new<A: ToSocketAddrs>(addr: A) -> SenderBuilder {
|
||||
SenderBuilder {
|
||||
server_addr: addr.to_socket_addrs().ok().expect("could not parse server address").next().unwrap(),
|
||||
credentials: None,
|
||||
connection_reuse_count_limit: 100,
|
||||
enable_connection_reuse: false,
|
||||
hello_name: "localhost".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new local SMTP client to port 25
|
||||
pub fn localhost() -> SenderBuilder {
|
||||
SenderBuilder::new(("localhost", SMTP_PORT))
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Build the SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Sender`
|
||||
pub fn build<S: Connecter + Read + Write>(self) -> Sender<S> {
|
||||
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<S = TcpStream> {
|
||||
/// 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<S>,
|
||||
}
|
||||
|
||||
macro_rules! try_smtp (
|
||||
($err: expr, $client: ident) => ({
|
||||
match $err {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
if !$client.state.panic {
|
||||
$client.state.panic = true;
|
||||
$client.client.close();
|
||||
}
|
||||
return Err(err)
|
||||
},
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
impl<S = TcpStream> Sender<S> {
|
||||
/// Creates a new SMTP client
|
||||
///
|
||||
/// It does not connects to the server, but only creates the `Sender`
|
||||
pub fn new(builder: SenderBuilder) -> Sender<S> {
|
||||
let client: Client<S> = Client::new(builder.server_addr);
|
||||
Sender{
|
||||
client: client,
|
||||
server_info: None,
|
||||
client_info: builder,
|
||||
state: State {
|
||||
panic: false,
|
||||
connection_reuse_count: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Connecter + Write + Read = TcpStream> Sender<S> {
|
||||
/// 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();
|
||||
}
|
||||
|
||||
/// Sends an email
|
||||
pub fn send<T: SendableEmail>(&mut self, mut 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());
|
||||
|
||||
// Log the connection
|
||||
info!("connection established to {}", self.client_info.server_addr);
|
||||
|
||||
// Extended Hello or Hello if needed
|
||||
match self.client.ehlo(&self.client_info.hello_name) {
|
||||
Ok(response) => {self.server_info = Some(
|
||||
ServerInfo{
|
||||
name: response.first_word().expect("Server announced no hostname"),
|
||||
esmtp_features: Extension::parse_esmtp_response(&response),
|
||||
});
|
||||
},
|
||||
Err(error) => match error {
|
||||
SmtpError::PermanentError(ref response) if response.has_code(550) => {
|
||||
match self.client.helo(&self.client_info.hello_name) {
|
||||
Ok(response) => {self.server_info = Some(
|
||||
ServerInfo{
|
||||
name: response.first_word().expect("Server announced no hostname"),
|
||||
esmtp_features: vec!(),
|
||||
});
|
||||
},
|
||||
Err(error) => try_smtp!(Err(error), self)
|
||||
}
|
||||
|
||||
},
|
||||
_ => {
|
||||
try_smtp!(Err(error), self)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Print server information
|
||||
debug!("server {}", self.server_info.as_ref().unwrap());
|
||||
}
|
||||
|
||||
// TODO: Use PLAIN AUTH in encrypted connections, CRAM-MD5 otherwise
|
||||
if self.client_info.credentials.is_some() && self.state.connection_reuse_count == 0 {
|
||||
|
||||
let (username, password) = self.client_info.credentials.clone().unwrap();
|
||||
|
||||
if self.server_info.as_ref().unwrap().supports_feature(Extension::CramMd5Authentication) {
|
||||
let result = self.client.auth_cram_md5(&username, &password);
|
||||
try_smtp!(result, self);
|
||||
} else if self.server_info.as_ref().unwrap().supports_feature(Extension::PlainAuthentication) {
|
||||
let result = self.client.auth_plain(&username, &password);
|
||||
try_smtp!(result, self);
|
||||
} else {
|
||||
debug!("No supported authentication mecanisms available");
|
||||
}
|
||||
}
|
||||
|
||||
let current_message = Uuid::new_v4();
|
||||
email.set_message_id(format!("<{}@{}>", current_message,
|
||||
self.client_info.hello_name.clone()));
|
||||
|
||||
let from_address = email.from_address();
|
||||
let to_addresses = email.to_addresses();
|
||||
let message = email.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(), match result.as_ref().ok().unwrap().message().as_slice() {
|
||||
[ref line, ..] => line.as_slice(),
|
||||
[] => "no response",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -1,85 +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.
|
||||
|
||||
//! Information about a server
|
||||
|
||||
use std::fmt;
|
||||
use std::fmt::{Display, Formatter};
|
||||
|
||||
use extension::Extension;
|
||||
|
||||
/// Contains information about an SMTP server
|
||||
#[derive(Clone,Debug)]
|
||||
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 esmtp_features: Vec<Extension>,
|
||||
}
|
||||
|
||||
impl Display for ServerInfo {
|
||||
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
|
||||
write!(f, "{} with {}",
|
||||
self.name,
|
||||
match self.esmtp_features.is_empty() {
|
||||
true => "no supported features".to_string(),
|
||||
false => format! ("{:?}", self.esmtp_features),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerInfo {
|
||||
/// Checks if the server supports an ESMTP feature
|
||||
pub fn supports_feature(&self, keyword: Extension) -> bool {
|
||||
self.esmtp_features.contains(&keyword)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::ServerInfo;
|
||||
use extension::Extension;
|
||||
|
||||
#[test]
|
||||
fn test_fmt() {
|
||||
assert_eq!(format!("{}", ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![Extension::EightBitMime]
|
||||
}), "name with [EightBitMime]".to_string());
|
||||
assert_eq!(format!("{}", ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![Extension::EightBitMime]
|
||||
}), "name with [EightBitMime]".to_string());
|
||||
assert_eq!(format!("{}", ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![]
|
||||
}), "name with no supported features".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supports_feature() {
|
||||
assert!(ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![Extension::EightBitMime]
|
||||
}.supports_feature(Extension::EightBitMime));
|
||||
assert!(ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![Extension::PlainAuthentication, Extension::EightBitMime]
|
||||
}.supports_feature(Extension::EightBitMime));
|
||||
assert_eq!(ServerInfo{
|
||||
name: "name".to_string(),
|
||||
esmtp_features: vec![Extension::EightBitMime]
|
||||
}.supports_feature(Extension::PlainAuthentication), false);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
1
website/.gitignore
vendored
Normal file
1
website/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/_book
|
||||
16
website/Makefile
Normal file
16
website/Makefile
Normal file
@@ -0,0 +1,16 @@
|
||||
all: depends _book
|
||||
|
||||
depends:
|
||||
cargo install --force mdbook --vers "^0.2"
|
||||
cargo install --force mdbook-linkcheck --vers "^0.2"
|
||||
|
||||
serve:
|
||||
mdbook serve
|
||||
|
||||
_book:
|
||||
mdbook build
|
||||
|
||||
clean:
|
||||
rm -rf _book/
|
||||
|
||||
.PHONY: _book
|
||||
11
website/book.toml
Normal file
11
website/book.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[book]
|
||||
title = "Lettre"
|
||||
author = "Alexis Mousset"
|
||||
description = "The user documentation of the Lettre crate."
|
||||
|
||||
[build]
|
||||
build-dir = "_book"
|
||||
|
||||
[output.html]
|
||||
|
||||
[output.linkcheck]
|
||||
19
website/src/README.md
Normal file
19
website/src/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Introduction
|
||||
|
||||
Lettre is an email library that allows creating and sending messages. It provides:
|
||||
|
||||
* An easy to use email builder
|
||||
* Pluggable email transports
|
||||
* Unicode support (for emails and transports, including for sender et recipient addresses when compatible)
|
||||
* Secure defaults (emails are only sent encrypted by default)
|
||||
|
||||
The `lettre_email` crate allows you to compose messages, and the `lettre`
|
||||
provide transports to send them.
|
||||
|
||||
Lettre requires Rust 1.32 or newer. Add the following to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lettre = "0.9"
|
||||
lettre_email = "0.9"
|
||||
```
|
||||
9
website/src/SUMMARY.md
Normal file
9
website/src/SUMMARY.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Summary
|
||||
|
||||
* [Introduction](README.md)
|
||||
* [Creating Messages](creating-messages/email.md)
|
||||
* [Sending Messages](sending-messages/_index.md)
|
||||
* [SMTP Transport](sending-messages/smtp.md)
|
||||
* [Sendmail Transport](sending-messages/sendmail.md)
|
||||
* [File Transport](sending-messages/file.md)
|
||||
* [Stub Transport](sending-messages/stub.md)
|
||||
36
website/src/creating-messages/email.md
Normal file
36
website/src/creating-messages/email.md
Normal file
@@ -0,0 +1,36 @@
|
||||
### Creating messages
|
||||
|
||||
This section explains how to create emails.
|
||||
|
||||
#### Simple example
|
||||
|
||||
The `email` part builds email messages. For now, it does not support attachments.
|
||||
An email is built using an `EmailBuilder`. The simplest email could be:
|
||||
|
||||
```rust
|
||||
extern crate lettre_email;
|
||||
|
||||
use lettre_email::Email;
|
||||
|
||||
fn main() {
|
||||
// Create an email
|
||||
let email = Email::builder()
|
||||
// Addresses can be specified by the tuple (email, alias)
|
||||
.to(("user@example.org", "Firstname Lastname"))
|
||||
// ... or by an address only
|
||||
.from("user@example.com")
|
||||
.subject("Hi, Hello world")
|
||||
.alternative("<h2>Hi, Hello world.</h2>", "Hi, Hello world.")
|
||||
.build();
|
||||
|
||||
assert!(email.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
When the `build` method is called, the `EmailBuilder` will add the missing headers (like
|
||||
`Message-ID` or `Date`) and check for missing necessary ones (like `From` or `To`). It will
|
||||
then generate an `Email` that can be sent.
|
||||
|
||||
The `text()` method will create a plain text email, while the `html()` method will create an
|
||||
HTML email. You can use the `alternative()` method to provide both versions, using plain text
|
||||
as fallback for the HTML version.
|
||||
17
website/src/sending-messages/_index.md
Normal file
17
website/src/sending-messages/_index.md
Normal file
@@ -0,0 +1,17 @@
|
||||
### 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 `SendableEmail`, which is the case for emails created with `lettre_email`.
|
||||
|
||||
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.
|
||||
43
website/src/sending-messages/file.md
Normal file
43
website/src/sending-messages/file.md
Normal file
@@ -0,0 +1,43 @@
|
||||
#### File Transport
|
||||
|
||||
The file transport writes the emails to the given directory. The name of the file will be
|
||||
`message_id.txt`.
|
||||
It can be useful for testing purposes, or if you want to keep track of sent messages.
|
||||
|
||||
```rust
|
||||
extern crate lettre;
|
||||
|
||||
use std::env::temp_dir;
|
||||
|
||||
use lettre::file::FileTransport;
|
||||
use lettre::{Transport, Envelope, EmailAddress, SendableEmail};
|
||||
|
||||
fn main() {
|
||||
// Write to the local temp directory
|
||||
let mut sender = FileTransport::new(temp_dir());
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
Example result in `/tmp/b7c211bc-9811-45ce-8cd9-68eab575d695.txt`:
|
||||
|
||||
```text
|
||||
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
|
||||
To: <root@localhost>
|
||||
From: <user@localhost>
|
||||
Subject: Hello
|
||||
Date: Sat, 31 Oct 2015 13:42:19 +0100
|
||||
Message-ID: <b7c211bc-9811-45ce-8cd9-68eab575d695.lettre@localhost>
|
||||
|
||||
Hello World!
|
||||
```
|
||||
25
website/src/sending-messages/sendmail.md
Normal file
25
website/src/sending-messages/sendmail.md
Normal file
@@ -0,0 +1,25 @@
|
||||
#### Sendmail Transport
|
||||
|
||||
The sendmail transport sends the email using the local sendmail command.
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::sendmail::SendmailTransport;
|
||||
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let mut sender = SendmailTransport::new();
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
180
website/src/sending-messages/smtp.md
Normal file
180
website/src/sending-messages/smtp.md
Normal file
@@ -0,0 +1,180 @@
|
||||
#### SMTP Transport
|
||||
|
||||
This transport uses the SMTP protocol to send emails over the network (locally or remotely).
|
||||
|
||||
It is designed to be:
|
||||
|
||||
* Secured: email are encrypted by default
|
||||
* Modern: Unicode support for email content and sender/recipient addresses when compatible
|
||||
* Fast: supports tcp connection reuse
|
||||
|
||||
This client is designed to send emails to a relay server, and should *not* be used to send
|
||||
emails directly to the destination.
|
||||
|
||||
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
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::{SendableEmail, EmailAddress, Transport, Envelope, SmtpClient};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
// Open a local connection on port 25
|
||||
let mut mailer =
|
||||
SmtpClient::new_unencrypted_localhost().unwrap().transport();
|
||||
// Send the email
|
||||
let result = mailer.send(email);
|
||||
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
#### Complete example
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::{SendableEmail, Envelope, EmailAddress, Transport, SmtpClient};
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
|
||||
fn main() {
|
||||
let email_1 = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id1".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let email_2 = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"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").unwrap()
|
||||
// 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:
|
||||
|
||||
```rust,no_run
|
||||
extern crate native_tls;
|
||||
extern crate lettre;
|
||||
extern crate lettre_email;
|
||||
|
||||
use lettre::{
|
||||
ClientSecurity, ClientTlsParameters, EmailAddress, Envelope,
|
||||
SendableEmail, SmtpClient, Transport,
|
||||
};
|
||||
use lettre::smtp::authentication::{Credentials, Mechanism};
|
||||
use lettre::smtp::ConnectionReuseParameters;
|
||||
use native_tls::{Protocol, TlsConnector};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"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().unwrap()
|
||||
);
|
||||
|
||||
let mut mailer = SmtpClient::new(
|
||||
("smtp.example.com", 465), ClientSecurity::Wrapper(tls_parameters)
|
||||
).unwrap()
|
||||
.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();
|
||||
}
|
||||
```
|
||||
|
||||
#### Lower level
|
||||
|
||||
You can also send commands, here is a simple email transaction without
|
||||
error handling:
|
||||
|
||||
```rust,no_run
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::EmailAddress;
|
||||
use lettre::smtp::SMTP_PORT;
|
||||
use lettre::smtp::client::InnerClient;
|
||||
use lettre::smtp::client::net::NetworkStream;
|
||||
use lettre::smtp::extension::ClientId;
|
||||
use lettre::smtp::commands::*;
|
||||
|
||||
fn main() {
|
||||
let mut email_client: InnerClient<NetworkStream> = InnerClient::new();
|
||||
let _ = email_client.connect(&("localhost", SMTP_PORT), None);
|
||||
let _ = email_client.command(EhloCommand::new(ClientId::new("my_hostname".to_string())));
|
||||
let _ = email_client.command(
|
||||
MailCommand::new(Some(EmailAddress::new("user@example.com".to_string()).unwrap()), vec![])
|
||||
);
|
||||
let _ = email_client.command(
|
||||
RcptCommand::new(EmailAddress::new("user@example.org".to_string()).unwrap(), vec![])
|
||||
);
|
||||
let _ = email_client.command(DataCommand);
|
||||
let _ = email_client.message(Box::new("Test email".as_bytes()));
|
||||
let _ = email_client.command(QuitCommand);
|
||||
}
|
||||
```
|
||||
|
||||
32
website/src/sending-messages/stub.md
Normal file
32
website/src/sending-messages/stub.md
Normal file
@@ -0,0 +1,32 @@
|
||||
#### Stub Transport
|
||||
|
||||
The stub transport only logs message envelope and drops the content. It can be useful for
|
||||
testing purposes.
|
||||
|
||||
```rust
|
||||
extern crate lettre;
|
||||
|
||||
use lettre::stub::StubTransport;
|
||||
use lettre::{SendableEmail, Envelope, EmailAddress, Transport};
|
||||
|
||||
fn main() {
|
||||
let email = SendableEmail::new(
|
||||
Envelope::new(
|
||||
Some(EmailAddress::new("user@localhost".to_string()).unwrap()),
|
||||
vec![EmailAddress::new("root@localhost".to_string()).unwrap()],
|
||||
).unwrap(),
|
||||
"id".to_string(),
|
||||
"Hello world".to_string().into_bytes(),
|
||||
);
|
||||
|
||||
let mut sender = StubTransport::new_positive();
|
||||
let result = sender.send(email);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
```
|
||||
|
||||
Will log (when using a logger like `env_logger`):
|
||||
|
||||
```text
|
||||
b7c211bc-9811-45ce-8cd9-68eab575d695: from=<user@localhost> to=<root@localhost>
|
||||
```
|
||||
BIN
website/theme/favicon.png
Normal file
BIN
website/theme/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user