diff --git a/Cargo.lock b/Cargo.lock index ace0a83..3086a68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,12 +29,31 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[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]] name = "async-stream" version = "0.3.5" @@ -89,6 +108,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.0" @@ -107,6 +132,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.6.0" @@ -151,6 +182,24 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[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]] name = "document-features" version = "0.2.8" @@ -168,11 +217,13 @@ dependencies = [ "document-features", "lettre", "log", + "reqwest", "secrecy", "serde", "thiserror", "tokio", "tokio-test", + "wiremock", ] [[package]] @@ -181,7 +232,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60d1d33cdaede7e24091f039632eb5d3c7469fe5b066a985281a34fc70fa317f" dependencies = [ - "base64", + "base64 0.22.0", "memchr", ] @@ -191,6 +242,21 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -207,6 +273,12 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -231,18 +303,71 @@ dependencies = [ "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]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" 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", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + [[package]] name = "futures-task" version = "0.3.30" @@ -255,8 +380,11 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -270,6 +398,25 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.14.3" @@ -297,12 +444,109 @@ dependencies = [ "winapi", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + [[package]] name = "httpdate" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hyper" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2", + "tokio", + "tower", + "tower-service", + "tracing", +] + [[package]] name = "idna" version = "0.5.0" @@ -313,6 +557,37 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -326,7 +601,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47460276655930189e0919e4fbf46e46476b14f934f18a63dd726a5fb7b60e2e" dependencies = [ "async-trait", - "base64", + "base64 0.22.0", "chumsky", "email-encoding", "email_address", @@ -519,6 +794,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -570,6 +865,77 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.0", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -589,6 +955,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beb461507cee2c2ff151784c52762cf4d9ff6a61f3e80968600ed24fa837fa54" + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + [[package]] name = "schannel" version = "0.1.23" @@ -651,6 +1039,29 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "slab" version = "0.4.9" @@ -660,6 +1071,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + [[package]] name = "socket2" version = "0.5.6" @@ -694,6 +1111,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.10.1" @@ -748,6 +1192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", + "bytes", "libc", "mio", "num_cpus", @@ -802,12 +1247,55 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + [[package]] name = "tracing" version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ + "log", "pin-project-lite", "tracing-core", ] @@ -821,6 +1309,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -865,12 +1359,97 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1032,6 +1611,40 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "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 0.21.7", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 0b1ef07..8287138 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ lettre = { version = "0.11.6", features = ["tracing", "tokio1-native-tls", "toki thiserror = "1.0.58" log = "0.4.21" document-features = { version = "0.2", optional = true } +reqwest = { version = "0.12.4", optional = true, features = ["json"] } @@ -39,19 +40,20 @@ memory = [] ### Enable smtp client based on lettre. smtp = ["dep:secrecy", "dep:lettre"] -### Upcoming feature for mailsend. -mailsend = [] +### Send email using mailersend +mailersend = ["dep:secrecy", "dep:reqwest"] [dev-dependencies] tokio-test = "0.4.4" +wiremock = "0.6.0" [package.metadata.cargo-udeps.ignore] normal = ["log"] -development = ["tokio-test"] +development = ["tokio-test", "wiremock"] # docs.rs-specific configuration [package.metadata.docs.rs] # document all features all-features = true # defines the configuration attribute `docsrs` -rustdoc-args = ["--cfg", "docsrs"] \ No newline at end of file +rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 52f886b..0884890 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Based on the email client you want to support, you need to initialize email conf ```rust async fn send_email() { let email = EmailObject { - sender: "test@example.com".to_string(), + sender: "test@example.com", to: vec![EmailAddress { name: "Mail".to_string(), email: "to@example.com".to_string() }], subject: "subject".to_string(), plain: "plain body".to_string(), diff --git a/deny.toml b/deny.toml index b9d4b9d..800cf05 100644 --- a/deny.toml +++ b/deny.toml @@ -79,6 +79,7 @@ allow = [ "MIT", "Apache-2.0", "Unicode-DFS-2016", + "BSD-3-Clause", "0BSD" #"Apache-2.0 WITH LLVM-exception", ] diff --git a/src/clients/mailersend.rs b/src/clients/mailersend.rs new file mode 100644 index 0000000..868ff63 --- /dev/null +++ b/src/clients/mailersend.rs @@ -0,0 +1,257 @@ +use crate::configuration::EmailConfiguration; +use crate::email::{EmailAddress, EmailObject}; +use crate::traits::EmailTrait; +use crate::Result; +use async_trait::async_trait; +use reqwest::header::HeaderMap; +use reqwest::{header, Client, Method}; +use secrecy::{ExposeSecret, Secret}; + +static BASE_URL: &str = "https://api.mailersend.com/v1"; + +fn default_base_url() -> String { + BASE_URL.to_string() +} + +/// `MailerSendConfig` structure that includes sender, base_url, and api_token. +/// +/// ```rust +/// use email_clients::clients::mailersend::MailerSendConfig; +/// +/// let mut mailer_send_config = MailerSendConfig::default() +/// .sender("sender@example.com") +/// .base_url("https://api.mailersend.com/v1") +/// .api_token("test_api_token"); +/// assert_eq!(mailer_send_config.get_sender().to_string(), "sender@example.com"); +/// assert_eq!(mailer_send_config.get_base_url(), "https://api.mailersend.com/v1"); +/// ``` +#[derive(Debug, Clone, serde::Deserialize)] +pub struct MailerSendConfig { + sender: EmailAddress, + #[serde(default = "default_base_url")] + base_url: String, + api_token: Secret, +} + +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +struct EmailPayload { + from: EmailAddress, + to: Vec, + subject: String, + text: String, + html: String, +} + +impl From for EmailPayload { + fn from(value: EmailObject) -> Self { + Self { + from: value.sender, + to: value.to, + subject: value.subject, + text: value.plain, + html: value.html, + } + } +} + +impl Default for MailerSendConfig { + /// Constructs a `MailerSendConfig` with default values: + /// - sender: An empty string `""` + /// - base_url: `https://api.mailersend.com/v1` + /// - api_token: An empty string `""` + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let config = MailerSendConfig::default(); + /// + /// assert_eq!(config.get_sender().to_string(), ""); + /// assert_eq!(config.get_base_url(), "https://api.mailersend.com/v1"); + /// ``` + /// + fn default() -> Self { + Self { + sender: "".into(), + base_url: BASE_URL.to_string(), + api_token: Secret::from("".to_string()), + } + } +} + +impl MailerSendConfig { + /// Sets the sender of the Mailersend config. + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let mut smtp_config = MailerSendConfig::default().sender("Test Sender"); + /// assert_eq!(smtp_config.get_sender().to_string(), "Test Sender"); + /// ``` + pub fn sender(mut self, value: impl Into) -> Self { + self.sender = value.into(); + self + } + + /// Sets the base_url of the Mailersend config. + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let mut smtp_config = MailerSendConfig::default().base_url("Test URL"); + /// assert_eq!(smtp_config.get_base_url(), "Test URL"); + /// ``` + pub fn base_url(mut self, value: impl AsRef) -> Self { + self.base_url = value.as_ref().trim_end_matches('/').to_string(); + self + } + + /// Sets the api_token of the Mailersend config. + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let mut smtp_config = MailerSendConfig::default().api_token("Test Token"); + /// ``` + pub fn api_token(mut self, value: impl AsRef) -> Self { + self.api_token = Secret::new(value.as_ref().to_string()); + self + } + + /// Returns the base url of the Mailersend config. + /// + /// # Example + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let smtp_config = MailerSendConfig::default().base_url("https://api.mailersend.com/v1"); + /// assert_eq!(smtp_config.get_base_url(), "https://api.mailersend.com/v1"); + /// ``` + /// + pub fn get_base_url(&self) -> String { + self.base_url.to_string() + } + + /// Returns the sender of the Mailersend config. + /// + /// # Example + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// + /// let mailer_send_config = MailerSendConfig::default().sender("test_sender@example.com"); + /// assert_eq!(mailer_send_config.get_sender().to_string(), "test_sender@example.com"); + /// ``` + /// + pub fn get_sender(&self) -> EmailAddress { + self.sender.clone() + } +} + +impl From for EmailConfiguration { + /// Converts a `MailerSendConfig` into an `EmailConfiguration` + /// + /// This conversion is mainly used when we are setting the configuration for our email client. + /// + /// # Example + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// use email_clients::configuration::EmailConfiguration; + /// + /// let mailer_config = MailerSendConfig::default() + /// .sender("sender@example.com") + /// .base_url("https://api.mailersend.com/v1") + /// .api_token("test_api_token"); + /// + /// let email_config: EmailConfiguration = mailer_config.into(); + /// ``` + fn from(value: MailerSendConfig) -> Self { + EmailConfiguration::Mailersend(value) + } +} + +/// `MailerSendClient` structure that includes 'config' and 'reqwest_client'. +/// +/// ```rust +/// use email_clients::clients::mailersend::MailerSendConfig; +/// use email_clients::clients::mailersend::MailerSendClient; +/// +/// let mailer_send_config = MailerSendConfig::default() +/// .sender("sender@example.com") +/// .base_url("https://api.mailersend.com/v1") +/// .api_token("test_api_token"); +/// let mailer_send_client = MailerSendClient::new(mailer_send_config); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct MailerSendClient { + config: MailerSendConfig, + reqwest_client: Client, +} + +impl MailerSendClient { + pub fn new(config: MailerSendConfig) -> Self { + let reqwest_client = Client::new(); + + MailerSendClient { + config, + reqwest_client, + } + } + + fn url(&self) -> String { + format!("{}/email", self.config.base_url.trim_end_matches('/')) + } + + fn headers(&self) -> Result { + let mut headers = HeaderMap::new(); + headers.insert( + header::AUTHORIZATION, + format!("Bearer {}", self.config.api_token.expose_secret()).parse()?, + ); + Ok(headers) + } +} + +#[async_trait] +impl EmailTrait for MailerSendClient { + /// Returns the sender included in the `MailerSendClient`'s configuration. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// use email_clients::clients::mailersend::MailerSendConfig; + /// use email_clients::clients::mailersend::MailerSendClient; + /// use email_clients::traits::EmailTrait; + /// + /// let mailer_send_config = MailerSendConfig::default() + /// .sender("sender@example.com") + /// .base_url("https://api.mailersend.com/v1") + /// .api_token("test_api_token"); + /// + /// let mailer_send_client = MailerSendClient::new(mailer_send_config); + /// + /// assert_eq!(mailer_send_client.get_sender().to_string(), "sender@example.com"); + /// ``` + fn get_sender(&self) -> EmailAddress { + self.config.get_sender().clone() + } + + async fn send_emails(&self, email: EmailObject) -> Result<()> { + let payload: EmailPayload = email.into(); + self.reqwest_client + .request(Method::POST, self.url()) + .headers(self.headers()?) + .json(&payload) + .send() + .await? + .error_for_status()?; + Ok(()) + } +} diff --git a/src/clients/memory.rs b/src/clients/memory.rs index d06346d..dc82cb3 100644 --- a/src/clients/memory.rs +++ b/src/clients/memory.rs @@ -3,13 +3,13 @@ use async_trait::async_trait; use std::sync::mpsc; use std::sync::mpsc::SyncSender; -use crate::email::EmailObject; +use crate::email::{EmailAddress, EmailObject}; use crate::errors::EmailError; use crate::traits::EmailTrait; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default, PartialOrd, PartialEq)] pub struct MemoryConfig { - pub sender: String, + pub sender: EmailAddress, } impl MemoryConfig { @@ -26,11 +26,11 @@ impl MemoryConfig { /// use email_clients::clients::memory::MemoryConfig; /// /// let config = MemoryConfig::new("sender@example.com"); - /// assert_eq!(config.sender, "sender@example.com"); + /// assert_eq!(config.sender.to_string(), "sender@example.com"); /// ``` - pub fn new(sender: impl AsRef) -> Self { + pub fn new(sender: impl Into) -> Self { Self { - sender: sender.as_ref().to_string(), + sender: sender.into(), } } } @@ -49,10 +49,10 @@ impl From for MemoryConfig { /// use email_clients::clients::memory::MemoryConfig; /// let value = String::from("sender@example.com"); /// let config = MemoryConfig::from(value); - /// assert_eq!(config.sender, "sender@example.com"); + /// assert_eq!(config.sender.to_string(), "sender@example.com"); /// ``` fn from(value: String) -> Self { - Self::new(value) + Self::new(value.as_str()) } } @@ -70,7 +70,7 @@ impl From for EmailConfiguration { /// # /// # match email_config { /// # EmailConfiguration::Memory(mc) => { - /// # assert_eq!(mc.sender, "sender@example.com"); + /// # assert_eq!(mc.sender.to_string(), "sender@example.com"); /// # }, /// # _ => panic!("Invalid conversion"), /// # } @@ -82,7 +82,7 @@ impl From for EmailConfiguration { #[derive(Clone, Debug)] pub struct MemoryClient { - sender: String, + sender: EmailAddress, tx: SyncSender, } @@ -103,12 +103,12 @@ impl Default for MemoryClient { /// /// let default_client = MemoryClient::default(); /// // Gets the default sender which is an empty string - /// assert_eq!(default_client.get_sender(), ""); + /// assert_eq!(default_client.get_sender().to_string(), ""); /// ``` fn default() -> Self { let (tx, _) = mpsc::sync_channel(5 /* usize */); Self { - sender: "".to_string(), + sender: "".into(), tx, } } @@ -130,7 +130,7 @@ impl MemoryClient { /// /// let config = MemoryConfig::new("sender@example.com"); /// let client = MemoryClient::new(config); - /// assert_eq!(client.get_sender(), "sender@example.com"); + /// assert_eq!(client.get_sender().to_string(), "sender@example.com"); /// ``` pub fn new(config: MemoryConfig) -> Self { let (tx, _) = mpsc::sync_channel(5 /* usize */); @@ -160,7 +160,7 @@ impl MemoryClient { /// let config = MemoryConfig::new("sender@example.com"); /// let (tx, rx) = sync_channel(2); /// let client = MemoryClient::with_tx(config, tx.clone()); - /// assert_eq!(client.get_sender(), "sender@example.com"); + /// assert_eq!(client.get_sender().to_string(), "sender@example.com"); /// ``` pub fn with_tx(config: MemoryConfig, tx: SyncSender) -> Self { Self { @@ -172,10 +172,26 @@ impl MemoryClient { #[async_trait] impl EmailTrait for MemoryClient { - fn get_sender(&self) -> String { - self.sender.to_string() + /// Returns the sender email address used for the `MemoryClient`. + /// + /// # Returns + /// An `EmailAddress` that is used as the sender's email in the `MemoryClient`. + /// + /// # Examples + /// + /// ```rust + /// # use email_clients::clients::memory::{MemoryConfig, MemoryClient}; + /// # use email_clients::traits::EmailTrait; + /// + /// let config = MemoryConfig::new("sender@example.com"); + /// let client = MemoryClient::new(config); + /// assert_eq!(client.get_sender().to_string(), "sender@example.com"); + /// ``` + fn get_sender(&self) -> EmailAddress { + self.sender.clone() } + /// Sends email from memory client. async fn send_emails(&self, email: EmailObject) -> crate::Result<()> { self.tx .send(email) diff --git a/src/clients/mod.rs b/src/clients/mod.rs index 461f494..a04940d 100644 --- a/src/clients/mod.rs +++ b/src/clients/mod.rs @@ -1,4 +1,18 @@ +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] use crate::configuration::EmailConfiguration; +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] use crate::traits::EmailTrait; #[cfg_attr(docsrs, doc(cfg(feature = "smtp")))] @@ -13,6 +27,17 @@ pub mod memory; #[cfg(feature = "terminal")] pub mod terminal; +#[cfg_attr(docsrs, doc(cfg(feature = "mailersend")))] +#[cfg(feature = "mailersend")] +pub mod mailersend; + +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] ///`EmailClient` Enum representing different types of email clients. ///Currently supported email clients: SMTP, Terminal, Memory. /// @@ -21,33 +46,49 @@ pub mod terminal; /// To integrate SMTP email client: /// ///```rust +/// # #[cfg(feature = "smtp")]{ /// use email_clients::clients::EmailClient; /// use email_clients::clients::smtp::{SmtpClient, SmtpConfig}; -///let config = SmtpConfig::default(); -///let smtp_email_client = EmailClient::Smtp(SmtpClient::new(config)); +/// let config = SmtpConfig::default(); +/// let smtp_email_client = EmailClient::Smtp(SmtpClient::new(config)); +/// # } ///``` /// ///To integrate Terminal email client: /// ///```rust -///# use email_clients::clients::EmailClient; +/// # #[cfg(feature = "terminal")]{ +/// use email_clients::clients::EmailClient; /// use email_clients::configuration::EmailConfiguration::Terminal; -///# use email_clients::clients::terminal::{TerminalClient, TerminalConfig}; -///let config = TerminalConfig::default() ; -///let terminal_email_client = EmailClient::Terminal(TerminalClient::new(config)); +/// use email_clients::clients::terminal::{TerminalClient, TerminalConfig}; +/// let config = TerminalConfig::default() ; +/// let terminal_email_client = EmailClient::Terminal(TerminalClient::new(config)); +/// # } ///``` /// ///To integrate Memory email client: /// ///```rust +/// # #[cfg(feature = "memory")]{ /// use email_clients::clients::EmailClient; /// use email_clients::configuration::EmailConfiguration::Memory; /// use email_clients::clients::memory::{MemoryClient, MemoryConfig}; ///let config = MemoryConfig::default(); /// ///let memory_email_client = EmailClient::Memory(MemoryClient::new(config)); +/// # } ///``` /// +/// To integrate mailersend client: +/// +///```rust +/// # #[cfg(feature = "mailersend")]{ +/// use email_clients::clients::EmailClient; +/// use email_clients::clients::mailersend::{MailerSendClient, MailerSendConfig}; +/// +/// let config = MailerSendConfig::default().api_token("API_TOKEN"); +/// let mailersend_client = EmailClient::MailerSend(MailerSendClient::new(config)); +/// # } #[derive(Clone, Debug)] pub enum EmailClient { #[cfg(feature = "smtp")] @@ -56,6 +97,8 @@ pub enum EmailClient { Terminal(terminal::TerminalClient), #[cfg(feature = "memory")] Memory(memory::MemoryClient), + #[cfg(feature = "mailersend")] + MailerSend(mailersend::MailerSendClient), } #[cfg(feature = "terminal")] @@ -72,13 +115,20 @@ impl Default for EmailClient { /// ```rust /// # use email_clients::clients::EmailClient; /// let client = EmailClient::default(); - /// assert_eq!(client.unwrap().get_sender(), ""); + /// assert_eq!(client.unwrap().get_sender().to_string(), ""); /// ``` fn default() -> Self { EmailClient::Terminal(Default::default()) } } +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] pub fn get_email_client(configuration: EmailConfiguration) -> EmailClient { match configuration { #[cfg(feature = "terminal")] @@ -89,10 +139,51 @@ pub fn get_email_client(configuration: EmailConfiguration) -> EmailClient { } #[cfg(feature = "memory")] EmailConfiguration::Memory(c) => EmailClient::Memory(memory::MemoryClient::new(c)), + #[cfg(feature = "mailersend")] + EmailConfiguration::Mailersend(c) => { + EmailClient::MailerSend(mailersend::MailerSendClient::new(c)) + } } } +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] impl EmailClient { + /// Unwrap the `EmailClient` enum variant and convert it into a `Box`. + /// + /// This method allows us to obtain a Boxed trait object which implements + /// `EmailTrait` and `Send` from an instance of `EmailClient` regardless of its variant. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```rust + /// # #[cfg(feature = "smtp")]{ + /// # use email_clients::clients::EmailClient; + /// # use email_clients::clients::smtp::{SmtpClient, SmtpConfig}; + /// use email_clients::email::EmailObject; + /// # use email_clients::Result; + /// + /// # async fn run() -> Result<()> { + /// let config = SmtpConfig::default(); + /// let smtp_email_client = EmailClient::Smtp(SmtpClient::new(config)); + /// + /// // Unwrapping converts the specific variant into a Boxed trait object. + /// let unwrapped_client = smtp_email_client.unwrap(); + /// + /// // Now we can use unwrapped_client directly to use methods of EmailTrait. + /// unwrapped_client.send_emails(EmailObject::default()).await?; + /// # Ok(()) + /// # } + /// # } + /// # fn main() {} + /// ``` pub fn unwrap(self) -> Box { match self { #[cfg(feature = "smtp")] @@ -101,6 +192,8 @@ impl EmailClient { EmailClient::Terminal(c) => Box::new(c) as Box, #[cfg(feature = "memory")] EmailClient::Memory(c) => Box::new(c) as Box, + #[cfg(feature = "mailersend")] + EmailClient::MailerSend(c) => Box::new(c) as Box, } } } diff --git a/src/clients/smtp.rs b/src/clients/smtp.rs index 700c56f..62ac2c7 100644 --- a/src/clients/smtp.rs +++ b/src/clients/smtp.rs @@ -1,5 +1,5 @@ use crate::configuration::EmailConfiguration; -use crate::email::EmailObject; +use crate::email::{EmailAddress, EmailObject}; use crate::traits::EmailTrait; use async_trait::async_trait; use lettre::message::MultiPart; @@ -21,7 +21,7 @@ pub enum TlsMode { } #[derive(Debug, Clone, serde::Deserialize)] pub struct SmtpConfig { - pub sender: String, + pub sender: EmailAddress, pub relay: String, pub username: String, pub password: Secret, @@ -32,7 +32,7 @@ pub struct SmtpConfig { impl Default for SmtpConfig { fn default() -> Self { Self { - sender: "".to_string(), + sender: "".into(), relay: "localhost".to_owned(), username: "".to_string(), port: SMTP_PORT, @@ -49,10 +49,10 @@ impl SmtpConfig { /// use email_clients::clients::smtp::SmtpConfig; /// /// let mut smtp_config = SmtpConfig::default().sender("Test Sender"); - /// assert_eq!(smtp_config.sender, "Test Sender"); + /// assert_eq!(smtp_config.sender.to_string(), "Test Sender"); /// ``` - pub fn sender(mut self, value: impl AsRef) -> Self { - self.sender = value.as_ref().to_string(); + pub fn sender(mut self, value: impl Into) -> Self { + self.sender = value.into(); self } @@ -133,7 +133,7 @@ impl From for EmailConfiguration { /// use email_clients::clients::smtp::{SmtpConfig, TlsMode}; /// /// let smtp_config = SmtpConfig { - /// sender: "Test Sender".to_string(), + /// sender: "Test Sender".into(), /// relay: "Test Relay".to_string(), /// username: "Test User".to_string(), /// password: Secret::new("Test Password".to_string()), @@ -192,8 +192,8 @@ impl SmtpClient { #[async_trait] impl EmailTrait for SmtpClient { - fn get_sender(&self) -> String { - self.config.sender.to_string() + fn get_sender(&self) -> EmailAddress { + self.config.sender.clone() } async fn send_emails(&self, email: EmailObject) -> crate::Result<()> { @@ -201,8 +201,8 @@ impl EmailTrait for SmtpClient { let email_body = MultiPart::alternative_plain_html(email.plain, email.html); let mut message_builder = Message::builder() - .from(self.config.sender.parse()?) - .reply_to(self.config.sender.parse()?); + .from(self.get_sender().try_into()?) + .reply_to(self.get_sender().try_into()?); for addr in email.to { message_builder = message_builder.to(addr.try_into()?) } diff --git a/src/clients/terminal.rs b/src/clients/terminal.rs index ffe6efb..4e31e73 100644 --- a/src/clients/terminal.rs +++ b/src/clients/terminal.rs @@ -1,11 +1,11 @@ use crate::configuration::EmailConfiguration; -use crate::email::EmailObject; +use crate::email::{EmailAddress, EmailObject}; use crate::traits::EmailTrait; use async_trait::async_trait; #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default, PartialOrd, PartialEq)] pub struct TerminalConfig { - pub sender: String, + pub sender: EmailAddress, } impl From for TerminalConfig { @@ -22,16 +22,18 @@ impl From for TerminalConfig { /// use email_clients::clients::terminal::TerminalConfig; /// let value = String::from("sender@example.com"); /// let config = TerminalConfig::from(value); - /// assert_eq!(config.sender, "sender@example.com"); + /// assert_eq!(config.sender.to_string(), "sender@example.com"); /// ``` fn from(value: String) -> Self { - Self { sender: value } + Self { + sender: value.as_str().into(), + } } } #[derive(Clone, Debug, Default, PartialOrd, PartialEq)] pub struct TerminalClient { - sender: String, + sender: EmailAddress, } impl From for EmailConfiguration { @@ -75,8 +77,8 @@ impl TerminalClient { #[async_trait] impl EmailTrait for TerminalClient { - fn get_sender(&self) -> String { - self.sender.to_string() + fn get_sender(&self) -> EmailAddress { + self.sender.clone() } async fn send_emails(&self, email: EmailObject) -> crate::Result<()> { diff --git a/src/configuration.rs b/src/configuration.rs index e72df52..e81c41a 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -7,6 +7,9 @@ use crate::clients::terminal; #[cfg(feature = "memory")] use crate::clients::memory; +#[cfg(feature = "mailersend")] +use crate::clients::mailersend; + #[derive(Debug, Clone, serde::Deserialize)] pub enum EmailConfiguration { #[cfg(feature = "terminal")] @@ -15,6 +18,8 @@ pub enum EmailConfiguration { SMTP(smtp::SmtpConfig), // Use smtp passwords and options (all config) #[cfg(feature = "memory")] Memory(memory::MemoryConfig), // Use in memory client + #[cfg(feature = "mailersend")] + Mailersend(mailersend::MailerSendConfig), // Use mailersend client } #[cfg(feature = "terminal")] diff --git a/src/email.rs b/src/email.rs index 2834ad9..34a230e 100644 --- a/src/email.rs +++ b/src/email.rs @@ -2,16 +2,27 @@ use crate::errors::EmailError; #[cfg(feature = "smtp")] use lettre::message::Mailbox; +use std::fmt::Display; -#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default, PartialOrd, PartialEq)] pub struct EmailAddress { pub name: String, pub email: String, } +impl Display for EmailAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.name.is_empty() { + write!(f, "{}", self.email) + } else { + write!(f, "{} <{}>", self.name, self.email) + } + } +} + #[derive(serde::Serialize, serde::Deserialize, Clone, Debug, Default)] pub struct EmailObject { - pub sender: String, + pub sender: EmailAddress, pub to: Vec, pub subject: String, pub plain: String, @@ -29,3 +40,12 @@ impl TryInto for EmailAddress { }) } } + +impl From<&str> for EmailAddress { + fn from(value: &str) -> Self { + Self { + name: "".to_string(), + email: value.to_string(), + } + } +} diff --git a/src/errors.rs b/src/errors.rs index 08200d6..aa1756d 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -15,4 +15,10 @@ pub enum EmailError { SmtpError(#[from] lettre::transport::smtp::Error), #[error("Unexpected error: {0}")] UnexpectedError(String), + #[cfg(feature = "mailersend")] + #[error("Invalid api token for mailsend")] + MailsendHeaderError(#[from] reqwest::header::InvalidHeaderValue), + #[cfg(feature = "mailersend")] + #[error("Failed during making an API request: {0}")] + ReqwestError(#[from] reqwest::Error), } diff --git a/src/lib.rs b/src/lib.rs index 14d3252..c1d9f07 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! * Memory client for test cases //! * SMTP client with tls and starttls, local support //! * Easy configuration management +//! * Mailersend client with token and custom base url if needed. //! //! # Examples //! @@ -14,7 +15,10 @@ //! //!```rust //! use std::sync::mpsc; +//! # #[cfg(any(feature = "mailersend", feature = "terminal", feature = "smtp", feature = "memory", feature = "document-features", feature = "default"))] //! use email_clients::clients::{EmailClient, get_email_client}; +//! # #[cfg(feature = "mailersend")] +//! use email_clients::clients::mailersend::MailerSendConfig; //! # #[cfg(feature = "memory")] //! use email_clients::clients::memory::{MemoryClient, MemoryConfig}; //! # #[cfg(feature = "smtp")] @@ -25,7 +29,7 @@ //! use email_clients::email::{EmailAddress, EmailObject}; //! //! let email = EmailObject { -//! sender: "test@example.com".to_string(), +//! sender: "test@example.com".into(), //! to: vec![EmailAddress { name: "Mail".to_string(), email: "to@example.com".to_string() }], //! subject: "subject".to_string(), //! plain: "plain body".to_string(), @@ -44,6 +48,10 @@ //! let (tx, rx) = mpsc::sync_channel(2); //! #[cfg(feature = "memory")] //! let memory_config: MemoryConfig = String::from("me@domain.com").into(); +//! // 4. Mailersend config (needs mailersend feature) +//! #[cfg(feature = "mailersend")] +//! let mailersend_config = MailerSendConfig::default().sender("sender@example.com").api_token("API_TOKEN"); +//! //! # #[cfg(feature = "terminal")] //! # { //! let email_configuration: EmailConfiguration = terminal_config.into(); // OR any of the other config diff --git a/src/traits.rs b/src/traits.rs index 53763f6..64ac523 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -1,9 +1,35 @@ -use crate::email::EmailObject; +use crate::email::{EmailAddress, EmailObject}; use async_trait::async_trait; #[async_trait] pub trait EmailTrait { - fn get_sender(&self) -> String; - + /// `EmailTrait` is a trait that outlines the basic capabilities for emailing. + /// It includes capabilities for getting the sender's email address and sending emails. + /// + /// # Examples + /// + /// ```no_run + /// use email_clients::email::{EmailAddress, EmailObject}; + /// use async_trait::async_trait; + /// + /// #[async_trait] + /// pub trait EmailTrait { + /// // Retrieves the sender's email address. + /// fn get_sender(&self) -> EmailAddress; + /// + /// // Sends an email. + /// async fn send_emails(&self, email: EmailObject) -> email_clients::Result<()> { + /// // Supposing we have a send_email method in our EmailObject. + /// // Ok(self.send_email(email)?) + /// Ok(()) + /// } + /// } + /// ``` + /// + /// # Notes + /// + /// - This trait must be implemented by all email utility classes. + /// - An instance of `EmailObject` passed to `send_emails` method should be a valid EmailObject + fn get_sender(&self) -> EmailAddress; async fn send_emails(&self, email: EmailObject) -> crate::Result<()>; } diff --git a/tests/clients.rs b/tests/clients.rs index 16da875..c79facb 100644 --- a/tests/clients.rs +++ b/tests/clients.rs @@ -1,8 +1,22 @@ +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] use email_clients::clients::get_email_client; #[cfg(feature = "memory")] use email_clients::clients::memory::MemoryConfig; #[cfg(feature = "smtp")] use email_clients::clients::smtp::SmtpConfig; +#[cfg(any( + feature = "mailersend", + feature = "terminal", + feature = "smtp", + feature = "memory", + feature = "document-features" +))] use email_clients::configuration::EmailConfiguration; #[cfg(feature = "terminal")] @@ -14,7 +28,7 @@ fn test_email_client_terminal() { let terminal_client = client.unwrap(); let sender = terminal_client.get_sender(); - assert_eq!(sender, ""); + assert_eq!(sender.to_string(), ""); } #[cfg(feature = "smtp")] @@ -26,7 +40,7 @@ fn test_email_client_smtp() { let smtp_client = client.unwrap(); let sender = smtp_client.get_sender(); - assert_eq!(sender, ""); + assert_eq!(sender.to_string(), ""); } #[cfg(feature = "memory")] @@ -38,5 +52,5 @@ fn test_email_client_memory() { let smtp_client = client.unwrap(); let sender = smtp_client.get_sender(); - assert_eq!(sender, ""); + assert_eq!(sender.to_string(), ""); } diff --git a/tests/mailersend.rs b/tests/mailersend.rs new file mode 100644 index 0000000..187342a --- /dev/null +++ b/tests/mailersend.rs @@ -0,0 +1,88 @@ +#[cfg(feature = "mailersend")] +mod test { + use email_clients::clients::get_email_client; + use email_clients::clients::mailersend::MailerSendConfig; + use email_clients::configuration::EmailConfiguration; + use email_clients::email::{EmailAddress, EmailObject}; + use wiremock::matchers::{bearer_token, method, path}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + #[tokio::test] + async fn send_email_using_mailersend_success() { + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/email")) + .and(bearer_token("API_TOKEN")) + .respond_with(ResponseTemplate::new(200)) + .expect(1..) + .mount(&mock_server) + .await; + + let recipient_mail = "mail@example.com".to_string(); + let mail_subject = "New subject".to_string(); + let mail_body = "Body of email".to_string(); + let mail_html = "Body of email in HTML".to_string(); + + let mailersend_config = MailerSendConfig::default() + .base_url(mock_server.uri()) + .api_token("API_TOKEN") + .sender("sender@example.com"); + + let email_configuration = EmailConfiguration::Mailersend(mailersend_config); + let email_client = get_email_client(email_configuration); + let email = EmailObject { + sender: "test@example.com".into(), + to: vec![EmailAddress { + name: "Mail".to_string(), + email: recipient_mail.clone(), + }], + subject: mail_subject.clone(), + plain: mail_body.clone(), + html: mail_html, + }; + + email_client + .unwrap() + .send_emails(email) + .await + .expect("Unable to send email"); + } + + #[tokio::test] + async fn send_email_using_mailersend_failure() { + let mock_server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/email")) + .and(bearer_token("API_TOKEN")) + .respond_with(ResponseTemplate::new(401)) + .expect(1..) + .mount(&mock_server) + .await; + + let recipient_mail = "mail@example.com".to_string(); + let mail_subject = "New subject".to_string(); + let mail_body = "Body of email".to_string(); + let mail_html = "Body of email in HTML".to_string(); + + let mailersend_config = MailerSendConfig::default() + .base_url(mock_server.uri()) + .api_token("API_TOKEN") + .sender("sender@example.com"); + + let email_configuration = EmailConfiguration::Mailersend(mailersend_config); + let email_client = get_email_client(email_configuration); + let email = EmailObject { + sender: "test@example.com".into(), + to: vec![EmailAddress { + name: "Mail".to_string(), + email: recipient_mail.clone(), + }], + subject: mail_subject.clone(), + plain: mail_body.clone(), + html: mail_html, + }; + + let response = email_client.unwrap().send_emails(email).await; + assert!(response.unwrap_err().to_string().starts_with("Failed during making an API request: HTTP status client error (401 Unauthorized) for url")); + } +} diff --git a/tests/memory.rs b/tests/memory.rs index ee746b3..7d3131f 100644 --- a/tests/memory.rs +++ b/tests/memory.rs @@ -11,15 +11,13 @@ mod test { let mail_subject = "New subject".to_string(); let mail_body = "Body of email".to_string(); let mail_html = "Body of email in HTML".to_string(); + let from: EmailAddress = "test@example.com".into(); let (tx, rx) = mpsc::sync_channel(2); - let email_client = EmailClient::Memory(MemoryClient::with_tx( - MemoryConfig::new("test@example.com"), - tx, - )); + let email_client = EmailClient::Memory(MemoryClient::with_tx(MemoryConfig::new(from), tx)); let email = EmailObject { - sender: "test@example.com".to_string(), + sender: "test@example.com".into(), to: vec![EmailAddress { name: "Mail".to_string(), email: recipient_mail.clone(), @@ -37,7 +35,7 @@ mod test { let email = rx.recv().unwrap(); - assert_eq!(email.sender, "test@example.com"); + assert_eq!(email.sender.email, "test@example.com"); assert_eq!(email.to[0].email, recipient_mail); assert_eq!(email.subject, mail_subject); assert_eq!(email.plain, mail_body); diff --git a/tests/smtp.rs b/tests/smtp.rs index c1ce3bf..2584616 100644 --- a/tests/smtp.rs +++ b/tests/smtp.rs @@ -15,7 +15,7 @@ mod test { let mail_html = "Body of email in HTML".to_string(); let smtp_config = SmtpConfig { - sender: "from@example.com".to_string(), + sender: "from@example.com".into(), relay: "127.0.0.1".to_string(), username: "".to_string(), password: Secret::from("".to_string()), @@ -25,7 +25,7 @@ mod test { let email_configuration = EmailConfiguration::SMTP(smtp_config); let email_client = get_email_client(email_configuration); let email = EmailObject { - sender: "test@example.com".to_string(), + sender: "test@example.com".into(), to: vec![EmailAddress { name: "Mail".to_string(), email: recipient_mail.clone(), diff --git a/tests/terminal.rs b/tests/terminal.rs index fe14f29..9d14d9d 100644 --- a/tests/terminal.rs +++ b/tests/terminal.rs @@ -15,7 +15,7 @@ mod test { let email_client = get_email_client(terminal_configuration); let email = EmailObject { - sender: "test@example.com".to_string(), + sender: "test@example.com".into(), to: vec![EmailAddress { name: "Mail".to_string(), email: recipient_mail.clone(),