email client introduced

This commit is contained in:
Andre Heber
2024-02-26 12:47:27 +01:00
parent e457729f61
commit 99b5e7fe30
12 changed files with 850 additions and 25 deletions

513
Cargo.lock generated
View File

@ -39,8 +39,8 @@ dependencies = [
"encoding_rs", "encoding_rs",
"flate2", "flate2",
"futures-core", "futures-core",
"h2", "h2 0.3.24",
"http", "http 0.2.11",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -75,7 +75,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511" checksum = "d22475596539443685426b6bdadb926ad0ecaefdfc5fb05e5e3441f15463c511"
dependencies = [ dependencies = [
"bytestring", "bytestring",
"http", "http 0.2.11",
"regex", "regex",
"serde", "serde",
"tracing", "tracing",
@ -254,6 +254,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.77" version = "0.1.77"
@ -341,6 +351,16 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bollard-stubs"
version = "1.42.0-rc.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864"
dependencies = [
"serde",
"serde_with",
]
[[package]] [[package]]
name = "brotli" name = "brotli"
version = "3.4.0" version = "3.4.0"
@ -419,6 +439,16 @@ dependencies = [
"windows-targets 0.52.0", "windows-targets 0.52.0",
] ]
[[package]]
name = "chumsky"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eebd66744a15ded14960ab4ccdbfb51ad3b81f51f3f04a80adac98c985396c9"
dependencies = [
"hashbrown 0.14.3",
"stacker",
]
[[package]] [[package]]
name = "claim" name = "claim"
version = "0.5.0" version = "0.5.0"
@ -580,6 +610,59 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "darling"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 1.0.109",
]
[[package]]
name = "darling_macro"
version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [
"darling_core",
"quote",
"syn 1.0.109",
]
[[package]]
name = "deadpool"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
dependencies = [
"async-trait",
"deadpool-runtime",
"num_cpus",
"tokio",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49"
[[package]] [[package]]
name = "der" name = "der"
version = "0.7.8" version = "0.7.8"
@ -655,6 +738,22 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "email-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75"
dependencies = [
"base64",
"memchr",
]
[[package]]
name = "email_address"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@ -756,6 +855,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.2.1" version = "1.2.1"
@ -765,6 +879,21 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.30" version = "0.3.30"
@ -809,6 +938,17 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.30"
@ -827,8 +967,10 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-channel",
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -885,7 +1027,26 @@ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http 0.2.11",
"indexmap",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 1.0.0",
"indexmap", "indexmap",
"slab", "slab",
"tokio", "tokio",
@ -966,6 +1127,17 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "hostname"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867"
dependencies = [
"libc",
"match_cfg",
"winapi",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.11" version = "0.2.11"
@ -977,6 +1149,17 @@ dependencies = [
"itoa", "itoa",
] ]
[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]] [[package]]
name = "http-body" name = "http-body"
version = "0.4.6" version = "0.4.6"
@ -984,7 +1167,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [ dependencies = [
"bytes", "bytes",
"http", "http 0.2.11",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http 1.0.0",
]
[[package]]
name = "http-body-util"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
dependencies = [
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"pin-project-lite", "pin-project-lite",
] ]
@ -1010,9 +1216,9 @@ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.24",
"http", "http 0.2.11",
"http-body", "http-body 0.4.6",
"httparse", "httparse",
"httpdate", "httpdate",
"itoa", "itoa",
@ -1024,6 +1230,27 @@ dependencies = [
"want", "want",
] ]
[[package]]
name = "hyper"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.2",
"http 1.0.0",
"http-body 1.0.0",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"smallvec",
"tokio",
"want",
]
[[package]] [[package]]
name = "hyper-rustls" name = "hyper-rustls"
version = "0.24.2" version = "0.24.2"
@ -1031,13 +1258,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"http", "http 0.2.11",
"hyper", "hyper 0.14.28",
"rustls", "rustls",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
] ]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"hyper 1.2.0",
"pin-project-lite",
"socket2",
"tokio",
]
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.60" version = "0.1.60"
@ -1061,6 +1304,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.4.0" version = "0.4.0"
@ -1156,6 +1405,31 @@ dependencies = [
"spin 0.5.2", "spin 0.5.2",
] ]
[[package]]
name = "lettre"
version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357ff5edb6d8326473a64c82cf41ddf78ab116f89668c50c4fac1b321e5e80f4"
dependencies = [
"base64",
"chumsky",
"email-encoding",
"email_address",
"fastrand",
"futures-util",
"hostname",
"httpdate",
"idna 0.5.0",
"mime",
"native-tls",
"nom",
"percent-encoding",
"quoted_printable",
"socket2",
"tokio",
"url",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.152" version = "0.2.152"
@ -1223,6 +1497,12 @@ version = "0.4.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
[[package]]
name = "match_cfg"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4"
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -1287,6 +1567,24 @@ version = "0.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e" checksum = "6d02c0b00610773bb7fc61d85e13d86c7858cbdf00e1a120bfc41bc055dbaa0e"
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.27.1" version = "0.27.1"
@ -1390,6 +1688,50 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae94056a791d0e1217d18b6cbdccb02c61e3054fc69893607f4067e3bb0b1fd1"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "ordered-multimap" name = "ordered-multimap"
version = "0.6.0" version = "0.6.0"
@ -1581,6 +1923,15 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "psm"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "quickcheck" name = "quickcheck"
version = "1.0.3" version = "1.0.3"
@ -1612,6 +1963,12 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "quoted_printable"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0"
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.5" version = "0.8.5"
@ -1706,10 +2063,10 @@ dependencies = [
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
"futures-util", "futures-util",
"h2", "h2 0.3.24",
"http", "http 0.2.11",
"http-body", "http-body 0.4.6",
"hyper", "hyper 0.14.28",
"hyper-rustls", "hyper-rustls",
"ipnet", "ipnet",
"js-sys", "js-sys",
@ -1856,6 +2213,15 @@ version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "schannel"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1882,6 +2248,29 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.21" version = "1.0.21"
@ -1940,6 +2329,28 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
dependencies = [
"serde",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.6" version = "0.10.6"
@ -2259,6 +2670,19 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "stacker"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"winapi",
]
[[package]] [[package]]
name = "stringprep" name = "stringprep"
version = "0.1.4" version = "0.1.4"
@ -2270,6 +2694,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.5.0"
@ -2332,6 +2762,32 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "testcontainers"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00"
dependencies = [
"bollard-stubs",
"futures",
"hex",
"hmac",
"log",
"rand",
"serde",
"serde_json",
"sha2",
]
[[package]]
name = "testcontainers-modules"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c391cd115649a8a14e5638d0606648d5348b216700a31f402987f57e58693766"
dependencies = [
"testcontainers",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.57" version = "1.0.57"
@ -3025,6 +3481,30 @@ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
[[package]]
name = "wiremock"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81"
dependencies = [
"assert-json-diff",
"async-trait",
"base64",
"deadpool",
"futures",
"http 1.0.0",
"http-body-util",
"hyper 1.2.0",
"hyper-util",
"log",
"once_cell",
"regex",
"serde",
"serde_json",
"tokio",
"url",
]
[[package]] [[package]]
name = "yaml-rust" name = "yaml-rust"
version = "0.4.5" version = "0.4.5"
@ -3043,6 +3523,7 @@ dependencies = [
"claim", "claim",
"config", "config",
"fake", "fake",
"lettre",
"once_cell", "once_cell",
"quickcheck", "quickcheck",
"quickcheck_macros", "quickcheck_macros",
@ -3050,7 +3531,10 @@ dependencies = [
"reqwest", "reqwest",
"secrecy", "secrecy",
"serde", "serde",
"serde_json",
"sqlx", "sqlx",
"testcontainers",
"testcontainers-modules",
"tokio", "tokio",
"tracing", "tracing",
"tracing-actix-web", "tracing-actix-web",
@ -3060,6 +3544,7 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
"uuid", "uuid",
"validator", "validator",
"wiremock",
] ]
[[package]] [[package]]

View File

@ -16,7 +16,7 @@ name = "zero2prod"
[dependencies] [dependencies]
actix-web = "4" actix-web = "4"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
reqwest = "0.11" reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
config = "0.14" config = "0.14"
uuid = { version = "1.7.0", features = ["v4"] } uuid = { version = "1.7.0", features = ["v4"] }
@ -31,12 +31,17 @@ tracing-actix-web = "0.7"
unicode-segmentation = "1" unicode-segmentation = "1"
claim = "0.5" claim = "0.5"
validator = "0.16" validator = "0.16"
lettre = "0.11"
[dev-dependencies] [dev-dependencies]
fake = "2.9" fake = "2.9"
quickcheck = "1.0" quickcheck = "1.0"
quickcheck_macros = "1.0" quickcheck_macros = "1.0"
rand = "0.8" rand = "0.8"
wiremock = "0.6"
serde_json = "1"
testcontainers = "0.15.0"
testcontainers-modules = { version = "0.3.4", features = ["postgres"] }
[dependencies.sqlx] [dependencies.sqlx]
version = "0.7.3" version = "0.7.3"

25
Makefile.toml Normal file
View File

@ -0,0 +1,25 @@
[tasks.format]
install_crate = "rustfmt"
command = "cargo"
args = ["fmt", "--", "--emit=files"]
[tasks.lint]
install_crate = "rust-clippy"
command = "cargo"
args = ["clippy"]
[tasks.fmtclip]
dependencies = [
"format",
"lint"
]
[tasks.machete]
install_crate = "cargo-machete"
command = "cargo"
args = ["machete"]
[tasks.audit]
install_crate = "cargo-audit"
command = "cargo"
args = ["audit"]

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# Zero2Prod
I'm going through the ebook "Zero to production in Rust" and this is the code for it!
## Makefile.toml
I stumbled over a [Rust tooling article](https://www.shuttle.rs/blog/2024/02/15/best-rust-tooling), therefore I installed, `cargo-make`, `cargo-audit` and other tools. To run the corresponding tool, look at the `Makefiel.toml` and run `cargo make xxx`!

View File

@ -6,3 +6,8 @@ database:
username: "postgres" username: "postgres"
password: "password" password: "password"
database_name: "newsletter" database_name: "newsletter"
email_client:
base_url: "localhost"
sender_email: "test@gmail.com"
authorization_token: "my-secret-token"
timeout_milliseconds: 10000

View File

@ -2,3 +2,6 @@ application:
host: 0.0.0.0 host: 0.0.0.0
database: database:
require_ssl: true require_ssl: true
email_client:
base_url: "https://api.postmarkapp.com"
sender_email: "andre@futureblog.eu"

View File

@ -2,10 +2,13 @@ use config::Config;
use secrecy::{ExposeSecret, Secret}; use secrecy::{ExposeSecret, Secret};
use sqlx::{postgres::{PgConnectOptions, PgSslMode}, ConnectOptions}; use sqlx::{postgres::{PgConnectOptions, PgSslMode}, ConnectOptions};
use crate::domain::SubscriberEmail;
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
pub struct Settings { pub struct Settings {
pub database: DatabaseSettings, pub database: DatabaseSettings,
pub application: ApplicationSettings, pub application: ApplicationSettings,
pub email_client: EmailClientSettings,
} }
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -45,6 +48,23 @@ impl DatabaseSettings {
} }
} }
#[derive(serde::Deserialize)]
pub struct EmailClientSettings {
pub base_url: String,
pub sender_email: String,
pub authorization_token: Secret<String>,
pub timeout_milliseconds: u64,
}
impl EmailClientSettings {
pub fn sender(&self) -> Result<SubscriberEmail, String> {
SubscriberEmail::parse(self.sender_email.clone())
}
pub fn timeout(&self) -> std::time::Duration {
std::time::Duration::from_millis(self.timeout_milliseconds)
}
}
pub fn get_configuration() -> Result<Settings, config::ConfigError> { pub fn get_configuration() -> Result<Settings, config::ConfigError> {
let run_mode = std::env::var("APP_ENVIRONMENT").unwrap_or("development".into()); let run_mode = std::env::var("APP_ENVIRONMENT").unwrap_or("development".into());
let base_path = std::env::current_dir().expect("Failed to determine the current directory"); let base_path = std::env::current_dir().expect("Failed to determine the current directory");

View File

@ -1,17 +1,197 @@
use reqwest::Client;
use secrecy::{ExposeSecret, Secret};
use crate::domain::SubscriberEmail; use crate::domain::SubscriberEmail;
pub struct EmailClient { pub struct EmailClient {
http_client: reqwest::Client,
base_url: String,
sender: SubscriberEmail, sender: SubscriberEmail,
authorization_token: Secret<String>,
} }
impl EmailClient { impl EmailClient {
pub fn new(
base_url: String,
sender: SubscriberEmail,
authorization_token: Secret<String>,
timeout: std::time::Duration,
) -> Self {
let http_client = Client::builder()
.timeout(timeout)
.build()
.expect("Failed to build reqwest client");
Self {
http_client,
base_url,
sender,
authorization_token,
}
}
pub async fn send_email( pub async fn send_email(
&self, &self,
recipient: SubscriberEmail, recipient: SubscriberEmail,
subject: &str, subject: &str,
html_content: &str, html_content: &str,
text_content: &str, text_content: &str,
) -> Result<(), String> { ) -> Result<(), reqwest::Error> {
todo!("send_email not implemented") let url = format!("{}/email", self.base_url);
let builder = self
.http_client
.post(&url)
.header("X-Postmark-Server-Token", self.authorization_token.expose_secret())
.json(&SendEmailRequest {
from: self.sender.as_ref(),
to: recipient.as_ref(),
subject,
html_body: html_content,
text_body: text_content,
})
.send()
.await?
.error_for_status()?;
Ok(())
}
}
#[derive(serde::Serialize)]
#[serde(rename_all = "PascalCase")]
struct SendEmailRequest<'a> {
from: &'a str,
to: &'a str,
subject: &'a str,
html_body: &'a str,
text_body: &'a str,
}
#[cfg(test)]
mod tests {
use claim::{assert_err, assert_ok};
use fake::{faker::{internet::en::SafeEmail, lorem::en::{Paragraph, Sentence}}, Fake, Faker};
use secrecy::Secret;
use wiremock::{http::Method, matchers::{any, header, header_exists, method, path}, Mock, MockServer, Request, ResponseTemplate};
use crate::domain::SubscriberEmail;
use super::EmailClient;
fn subject() -> String {
Sentence(1..2).fake()
}
fn content() -> String {
Paragraph(1..10).fake()
}
fn email() -> SubscriberEmail {
SubscriberEmail::parse(SafeEmail().fake()).unwrap()
}
fn email_client(base_url: String) -> EmailClient {
EmailClient::new(
base_url,
email(),
Secret::new(Faker.fake()),
std::time::Duration::from_millis(100),
)
}
#[tokio::test]
async fn send_email_fires_a_request_to_base_url() {
let mock_server = MockServer::start().await;
let email_client = email_client(mock_server.uri());
Mock::given(header_exists("X-Postmark-Server-Token"))
.and(header("Content-Type", "application/json"))
.and(path("/email"))
.and(method(Method::POST))
.and(SendEmailBodyMatcher)
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock_server)
.await;
let _ = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
}
struct SendEmailBodyMatcher;
impl wiremock::Match for SendEmailBodyMatcher {
fn matches(&self, request: &Request) -> bool {
let result: Result<serde_json::Value, _> = serde_json::from_slice(&request.body);
if let Ok(body) = result {
body.get("From").is_some()
&& body.get("To").is_some()
&& body.get("Subject").is_some()
&& body.get("HtmlBody").is_some()
&& body.get("TextBody").is_some()
} else {
false
}
}
}
#[tokio::test]
async fn send_email_succeeds_if_the_server_returns_200() {
let mock_server = MockServer::start().await;
let email_client = email_client(mock_server.uri());
Mock::given(any())
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock_server)
.await;
let outcome = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
assert_ok!(outcome);
}
#[tokio::test]
async fn send_email_fails_if_the_server_returns_500() {
let mock_server = MockServer::start().await;
let email_client = email_client(mock_server.uri());
Mock::given(any())
.respond_with(ResponseTemplate::new(500))
.expect(1)
.mount(&mock_server)
.await;
let outcome = email_client
.send_email(email(), &subject(), &content(), &content())
.await;
assert_err!(outcome);
}
#[tokio::test]
async fn send_email_times_out_if_the_server_takes_too_long() {
let mock_server = MockServer::start().await;
let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap();
let email_client = EmailClient::new(
mock_server.uri(),
sender,
Secret::new(Faker.fake()),
std::time::Duration::from_millis(100),
);
let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap();
let subject: String = Sentence(1..2).fake();
let content: String = Paragraph(1..10).fake();
Mock::given(any())
.respond_with(ResponseTemplate::new(200)
.set_delay(std::time::Duration::from_secs(180)))
.expect(1)
.mount(&mock_server)
.await;
let outcome = email_client
.send_email(subscriber_email, &subject, &content, &content)
.await;
assert_err!(outcome);
} }
} }

View File

@ -1,6 +1,7 @@
use std::net::TcpListener; use std::net::TcpListener;
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use zero2prod::configuration::get_configuration; use zero2prod::configuration::get_configuration;
use zero2prod::email_client;
use zero2prod::startup::run; use zero2prod::startup::run;
use zero2prod::telemetry::{get_subscriber, init_subscriber}; use zero2prod::telemetry::{get_subscriber, init_subscriber};
@ -10,11 +11,20 @@ async fn main() -> std::io::Result<()> {
init_subscriber(subscriber); init_subscriber(subscriber);
let config = get_configuration().expect("Failed to read configuration"); let config = get_configuration().expect("Failed to read configuration");
println!("Application configuration: {}", config.application.port);
let connection_pool = PgPoolOptions::new().acquire_timeout(std::time::Duration::from_secs(2)) let connection_pool = PgPoolOptions::new().acquire_timeout(std::time::Duration::from_secs(2))
.max_connections(10).connect_lazy_with(config.database.with_db()); .max_connections(10).connect_lazy_with(config.database.with_db());
let sender_email = config.email_client.sender().unwrap();
let timeout = config.email_client.timeout();
let email_client = email_client::EmailClient::new(
config.email_client.base_url,
sender_email,
config.email_client.authorization_token,
timeout,
);
let address = format!("{}:{}", config.application.host, config.application.port); let address = format!("{}:{}", config.application.host, config.application.port);
let listener = TcpListener::bind(address).expect("Failed to bind random port"); let listener = TcpListener::bind(address).expect("Failed to bind random port");
run(listener, connection_pool)?.await run(listener, connection_pool ,email_client)?.await
} }

View File

@ -3,6 +3,9 @@ use serde::Deserialize;
use chrono::Utc; use chrono::Utc;
use uuid::Uuid; use uuid::Uuid;
use sqlx::{query, Pool, Postgres}; use sqlx::{query, Pool, Postgres};
use lettre::{
address::AddressError, message::{header::ContentType, Mailbox}, transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport
};
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName}; use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
@ -26,11 +29,64 @@ pub async fn subscribe(form: web::Form<FormData>, connection_pool: web::Data<Poo
Err(_) => return HttpResponse::BadRequest().finish(), Err(_) => return HttpResponse::BadRequest().finish(),
}; };
match insert_subscriber(&new_subscriber, &connection_pool).await { match insert_subscriber(&new_subscriber, &connection_pool).await {
Ok(_) => (),
Err(_) => return HttpResponse::InternalServerError().finish(),
}
match send_mail(&new_subscriber) {
Ok(_) => HttpResponse::Ok().finish(), Ok(_) => HttpResponse::Ok().finish(),
Err(_) => HttpResponse::InternalServerError().finish(), Err(_) => HttpResponse::InternalServerError().finish(),
} }
} }
#[tracing::instrument(
name = "Sending confirmation email",
skip(new_subscriber)
)]
fn send_mail(new_subscriber: &NewSubscriber) -> Result<(), String> {
let from: Mailbox = "Andre Heber <andre@futureblog.eu>".parse().map_err(|e: AddressError| {
tracing::error!("Could not parse email - from: {:?}", e);
e.to_string()
})?;
let reply_to: Mailbox = "noreply@futureblog.eu".parse().map_err(|e: AddressError| {
tracing::error!("Could not parse email - reply_to: {:?}", e);
e.to_string()
})?;
let to: Mailbox = new_subscriber.email.as_ref().parse().map_err(|e: AddressError| {
tracing::error!("Could not parse email - to: {:?}", e);
e.to_string()
})?;
let email = Message::builder()
.from(from)
.reply_to(reply_to)
.to(to)
.subject("Rust Email")
.header(ContentType::TEXT_PLAIN)
.body(String::from("Hello, this is a test email from Rust!"))
.map_err(|e| {
tracing::error!("Could not build email: {:?}", e);
e.to_string()
})?;
let creds = Credentials::new("andre@futureblog.eu".to_string(), "d6vanPc4RUeQ".to_string());
let mailer = SmtpTransport::relay("mail.futureblog.eu")
.map_err(|e| {
tracing::error!("Could not connect to server: {:?}", e);
e.to_string()
})?;
let mailer = mailer.credentials(creds).build();
mailer.send(&email)
.map_err(|e| {
tracing::error!("Could not send email: {:?}", e);
e.to_string()
})?;
Ok(())
}
impl TryFrom<FormData> for NewSubscriber { impl TryFrom<FormData> for NewSubscriber {
type Error = String; type Error = String;

View File

@ -4,16 +4,23 @@ use sqlx::{Pool, Postgres};
use tracing_actix_web::TracingLogger; use tracing_actix_web::TracingLogger;
use std::net::TcpListener; use std::net::TcpListener;
use crate::email_client::EmailClient;
use crate::routes::{health_check, subscribe}; use crate::routes::{health_check, subscribe};
pub fn run(listener: TcpListener, connection_pool: Pool<Postgres>) -> Result<Server, std::io::Error> { pub fn run(
listener: TcpListener,
connection_pool: Pool<Postgres>,
email_client: EmailClient,
) -> Result<Server, std::io::Error> {
let connection_pool = web::Data::new(connection_pool); let connection_pool = web::Data::new(connection_pool);
let email_client = web::Data::new(email_client);
let server = HttpServer::new(move || { let server = HttpServer::new(move || {
App::new() App::new()
.wrap(TracingLogger::default()) .wrap(TracingLogger::default())
.route("/health_check", web::get().to(health_check)) .route("/health_check", web::get().to(health_check))
.route("/subscriptions", web::post().to(subscribe)) .route("/subscriptions", web::post().to(subscribe))
.app_data(connection_pool.clone()) .app_data(connection_pool.clone())
.app_data(email_client.clone())
}) })
.listen(listener)? .listen(listener)?
.run(); .run();

View File

@ -1,8 +1,10 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use secrecy::ExposeSecret;
use sqlx::{postgres::PgPoolOptions, query, Connection, Executor, PgConnection, Pool, Postgres}; use sqlx::{postgres::PgPoolOptions, query, Connection, Executor, PgConnection, Pool, Postgres};
use testcontainers::{clients::Cli, RunnableImage};
use uuid::Uuid; use uuid::Uuid;
use std:: net::TcpListener; use std:: net::TcpListener;
use zero2prod::{configuration::{get_configuration, DatabaseSettings}, telemetry::{get_subscriber, init_subscriber}}; use zero2prod::{configuration::{get_configuration, DatabaseSettings}, email_client, telemetry::{get_subscriber, init_subscriber}};
pub struct TestApp { pub struct TestApp {
pub address: String, pub address: String,
@ -22,6 +24,13 @@ static TRACING: Lazy<()> = Lazy::new(|| {
} }
}); });
fn create_db(config: &DatabaseSettings) -> RunnableImage<testcontainers_modules::postgres::Postgres> {
RunnableImage::from(testcontainers_modules::postgres::Postgres::default())
.with_env_var(("POSTGRES_PASSWORD", config.password.expose_secret()))
.with_env_var(("POSTGRES_USER", &config.username))
.with_env_var(("POSTGRES_DB", &config.database_name))
}
async fn spawn_app() -> TestApp { async fn spawn_app() -> TestApp {
Lazy::force(&TRACING); Lazy::force(&TRACING);
@ -30,10 +39,22 @@ async fn spawn_app() -> TestApp {
let address = format!("http://127.0.0.1:{}", port); let address = format!("http://127.0.0.1:{}", port);
let mut config = get_configuration().expect("Failed to read configuration"); let mut config = get_configuration().expect("Failed to read configuration");
let image = create_db(&config.database);
let docker = Cli::default();
docker.run(image);
config.database.database_name = Uuid::new_v4().to_string(); config.database.database_name = Uuid::new_v4().to_string();
let connection_pool = configure_database(&config.database).await; let connection_pool = configure_database(&config.database).await;
let server = zero2prod::startup::run(listener, connection_pool.clone()).expect("Failed to bind address"); let sender_email = config.email_client.sender().unwrap();
let timeout = config.email_client.timeout();
let email_client = email_client::EmailClient::new(
config.email_client.base_url,
sender_email,
config.email_client.authorization_token,
timeout,
);
let server = zero2prod::startup::run(listener, connection_pool.clone(), email_client).expect("Failed to bind address");
let _ = tokio::spawn(server); let _ = tokio::spawn(server);
TestApp { TestApp {
@ -85,7 +106,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() {
let app = spawn_app().await; let app = spawn_app().await;
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let body = "name=andre&email=andre.heber@gmx.net";
let response = client let response = client
.post(&format!("{}/subscriptions", &app.address)) .post(&format!("{}/subscriptions", &app.address))
.header("Content-Type", "application/x-www-form-urlencoded") .header("Content-Type", "application/x-www-form-urlencoded")
@ -101,8 +122,8 @@ async fn subscribe_returns_a_200_for_valid_form_data() {
.await .await
.expect("Failed to fetch saved subscription."); .expect("Failed to fetch saved subscription.");
assert_eq!(saved.email, "ursula_le_guin@gmail.com"); assert_eq!(saved.email, "andre.heber@gmx.net");
assert_eq!(saved.name, "le guin"); assert_eq!(saved.name, "andre");
} }
#[tokio::test] #[tokio::test]