From 1b6f544e550ef09b9af3770368a49047fe58e25a Mon Sep 17 00:00:00 2001 From: Josef Rokos Date: Mon, 12 Feb 2024 17:17:26 +0100 Subject: [PATCH] Implemented mail notifications. --- Cargo.lock | 456 ++++++++++++++++++++++++++++++++----- Cargo.toml | 4 +- src/backend/mail.rs | 74 ++++++ src/backend/mod.rs | 18 +- src/backend/reservation.rs | 121 ++++++++-- src/backend/user.rs | 31 +++ src/config.rs | 51 ++++- src/error.rs | 36 ++- src/main.rs | 5 +- 9 files changed, 719 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd813a..6b18ebc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -331,6 +331,54 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.71" @@ -668,7 +716,17 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets", + "windows-targets 0.48.0", +] + +[[package]] +name = "chumsky" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23170228b96236b5a7299057ac284a321457700bc8c41a4476052f0f4ba5349d" +dependencies = [ + "hashbrown 0.12.3", + "stacker", ] [[package]] @@ -723,6 +781,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "186dce98367766de751c42c4f03970fc60fc012296e706ccbb9d5df9b6c1e271" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "config" version = "0.13.3" @@ -817,6 +881,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -1077,6 +1151,22 @@ dependencies = [ "serde", ] +[[package]] +name = "email-encoding" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" +dependencies = [ + "base64 0.21.2", + "memchr", +] + +[[package]] +name = "email_address" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" + [[package]] name = "encoding_rs" version = "0.8.32" @@ -1086,17 +1176,27 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" -version = "0.10.1" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "05e7cf40684ae96ade6232ed84582f40ce0a66efcd43a5117aef610534f8e0b8" dependencies = [ + "anstream", + "anstyle", + "env_filter", "humantime", - "is-terminal", "log", - "regex", - "termcolor", ] [[package]] @@ -1113,7 +1213,7 @@ checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1134,7 +1234,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1177,6 +1277,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "form_urlencoded" version = "1.2.0" @@ -1493,12 +1608,6 @@ dependencies = [ "libc", ] -[[package]] -name = "hermit-abi" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" - [[package]] name = "hex" version = "0.4.3" @@ -1539,7 +1648,18 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys", + "windows-sys 0.48.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]] @@ -1636,6 +1756,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "if_chain" version = "1.0.2" @@ -1692,17 +1822,6 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a53088c87cf71c9d4f3372a2cb9eea1e7b8a0b1bf8b7f7d23fe5b76dbb07e63b" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi 0.3.3", - "rustix", - "windows-sys", -] - [[package]] name = "itertools" version = "0.10.5" @@ -2012,6 +2131,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "lettre" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a48c2e9831b370bc2d7233c2620298c45f3a158ed6b4b8d7416b2ada5a268fd8" +dependencies = [ + "async-trait", + "base64 0.21.2", + "chumsky", + "email-encoding", + "email_address", + "fastrand", + "futures-io", + "futures-util", + "hostname", + "httpdate", + "idna 0.5.0", + "mime", + "native-tls", + "nom", + "once_cell", + "quoted_printable", + "socket2 0.5.4", + "tokio", + "tokio-native-tls", + "url", + "uuid", +] + [[package]] name = "libc" version = "0.2.147" @@ -2117,6 +2265,12 @@ dependencies = [ "quote", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "md-5" version = "0.9.1" @@ -2183,7 +2337,25 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.48.0", +] + +[[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]] @@ -2261,7 +2433,7 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] @@ -2277,6 +2449,50 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "openssl" +version = "0.10.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" +dependencies = [ + "bitflags 2.4.0", + "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.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1bf214306098e4832460f797824c05d25aacdf896f64a985fb0fd992454ae" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "pad-adapter" version = "0.1.1" @@ -2303,7 +2519,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.48.0", ] [[package]] @@ -2486,6 +2702,15 @@ dependencies = [ "yansi", ] +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2542,6 +2767,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "quoted_printable" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79ec282e887b434b68c18fe5c121d38e72a5cf35119b59e54ec5b992ea9c8eb0" + [[package]] name = "radium" version = "0.7.0" @@ -2644,6 +2875,7 @@ dependencies = [ "leptos_actix", "leptos_meta", "leptos_router", + "lettre", "log", "pwhash", "regex", @@ -2778,7 +3010,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2836,6 +3068,15 @@ dependencies = [ "winapi-util", ] +[[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]] name = "scopeguard" version = "1.1.0" @@ -2858,6 +3099,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[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]] name = "self_cell" version = "1.0.1" @@ -3132,7 +3396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -3381,6 +3645,19 @@ dependencies = [ "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]] name = "stringprep" version = "0.1.3" @@ -3453,16 +3730,7 @@ dependencies = [ "fastrand", "redox_syscall", "rustix", - "windows-sys", -] - -[[package]] -name = "termcolor" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" -dependencies = [ - "winapi-util", + "windows-sys 0.48.0", ] [[package]] @@ -3541,7 +3809,17 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2 0.4.9", - "windows-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", ] [[package]] @@ -3748,7 +4026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", ] @@ -3758,6 +4036,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" version = "1.4.1" @@ -3774,7 +4058,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "idna", + "idna 0.4.0", "lazy_static", "regex", "serde", @@ -3979,7 +4263,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", ] [[package]] @@ -3988,7 +4272,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", ] [[package]] @@ -3997,13 +4290,28 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -4012,42 +4320,84 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + [[package]] name = "winnow" version = "0.5.19" diff --git a/Cargo.toml b/Cargo.toml index 1f2c500..dc7c9a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,9 +30,10 @@ futures-util = "0.3.28" regex = "1.10.2" toml = "0.8.8" log = "0.4.20" -env_logger = "0.10.1" +env_logger = "0.11" getopts = "0.2.21" leptos-use = "0.10.1" +lettre = {version = "0.11", features = ["tokio1-native-tls", "smtp-transport", "file-transport"], optional = true} [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] @@ -43,6 +44,7 @@ ssr = [ "dep:leptos_actix", "dep:actix-session", "dep:sqlx", + "dep:lettre", "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", diff --git a/src/backend/mail.rs b/src/backend/mail.rs index 4cccd63..e332cd6 100644 --- a/src/backend/mail.rs +++ b/src/backend/mail.rs @@ -10,6 +10,13 @@ cfg_if! { if #[cfg(feature = "ssr")] { use crate::backend::get_pool; use crate::error::AppError; use log::info; + use crate::config::Mailing; + use crate::config::MailTransport; + use crate::backend::data::ResSumWithItems; + use lettre::message::Message as LettreMessage; + use lettre::{AsyncSmtpTransport, AsyncFileTransport, AsyncTransport, Tokio1Executor}; + use lettre::transport::smtp::authentication::Credentials; + use std::ops::Add; pub async fn message_for_type(msg_type: &MessageType, pool: &PgPool) -> Result { Ok(query_as::<_, Message>("SELECT * FROM message WHERE msg_type = $1") @@ -48,6 +55,73 @@ cfg_if! { if #[cfg(feature = "ssr")] { Ok(()) } + + pub struct MailMessage { + reply_to: String, + to: String, + subject: String, + text: String + } + + impl MailMessage { + pub fn new(reply_to: String, to: String, message: Message, reservation: &ResSumWithItems) -> Self { + Self { + reply_to, + to, + subject: message.subject, + text: Self::replace_body_vars(message.text, &reservation) + } + } + + fn replace_body_vars(text: String, reservation: &ResSumWithItems) -> String { + text + .replace("#date#", &reservation.summary.date.format("%d. %m. %Y").to_string()) + .replace("#summary#", &{ + let mut sum = "".to_string(); + for p in &reservation.reservations { + sum = sum.add(&format!("{}: {} - {}", + p.property.name, + p.reservation.from.format("%H:%M").to_string(), p.reservation.to.format("%H:%M").to_string())); + } + sum + } + ) + } + + pub fn build_mail(&self, from: String) -> Result { + Ok(LettreMessage::builder() + .from(from.parse()?) + .reply_to(self.reply_to.parse()?) + .to(self.to.parse()?) + .subject(&self.subject) + .body(self.text.clone())?) + } + } + + impl Mailing { + pub async fn send_mail(&self, msg: MailMessage) -> Result<(), AppError> { + match self.transport() { + MailTransport::Smtp => { + let transport = if self.tls().unwrap_or(false) { + AsyncSmtpTransport::::starttls_relay(&self.server().clone().unwrap_or_default()) + } else { + AsyncSmtpTransport::::relay(&self.server().clone().unwrap_or_default()) + }.expect("Cannot create SMTP mail transport"); + if self.user().is_some() && self.password().is_some() { + let cred = Credentials::new(self.user().clone().unwrap(), self.password().clone().unwrap()); + transport.credentials(cred).build().send(msg.build_mail(self.from().to_string())?).await?; + } else { + transport.build().send(msg.build_mail(self.from().to_string())?).await?; + } + } + MailTransport::File => { + AsyncFileTransport::::new(self.path().clone().unwrap_or_default()) + .send(msg.build_mail(self.from().to_string())?).await?; + } + } + Ok(()) + } + } }} #[server] diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 1b5d9c4..112eedf 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -54,27 +54,39 @@ cfg_if!{ use actix_web::web::Data; use leptos_actix::extract; use leptos::ServerFnError; + use crate::config::Mailing; #[derive(Clone)] pub struct AppData { - db_pool: PgPool + db_pool: PgPool, + mailer: Mailing } impl AppData { - pub fn new(db_pool: PgPool) -> Self { + pub fn new(db_pool: PgPool, mailer: Mailing) -> Self { Self { - db_pool + db_pool, + mailer } } pub fn db_pool(&self) -> &PgPool { &self.db_pool } + + pub fn mailer(&self) -> &Mailing { + &self.mailer + } } pub async fn get_pool() -> Result { let data = extract::>().await?; Ok(data.db_pool().clone()) } + + pub async fn get_mailing() -> Result { + let data = extract::>().await?; + Ok(data.mailer().clone()) + } } } \ No newline at end of file diff --git a/src/backend/reservation.rs b/src/backend/reservation.rs index 1e624f2..b103e08 100644 --- a/src/backend/reservation.rs +++ b/src/backend/reservation.rs @@ -13,8 +13,16 @@ cfg_if! { if #[cfg(feature = "ssr")] { use std::ops::DerefMut; use std::str::FromStr; use futures_util::future::join_all; - use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer}; + use log::warn; + use crate::backend::data::{ReservationSum, ReservationState, ResWithProperty, Customer, Message, MessageType}; use crate::backend::get_pool; + use crate::backend::get_mailing; + use crate::backend::mail::MailMessage; + use crate::backend::mail::get_message; + use crate::error::AppError; + use sqlx::PgPool; + use crate::backend::user::admin_email; + use crate::backend::user::emails_for_notify; async fn find_sum_by_uuid(uuid: &Uuid, tx: &mut Transaction<'_, Postgres>) -> Result { let reservation = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1") @@ -43,6 +51,39 @@ cfg_if! { if #[cfg(feature = "ssr")] { } } + async fn reservation_by_uuid(uuid: Uuid) -> Result { + let pool = get_pool().await?; + let summary = query_as::<_, ReservationSum>("SELECT * FROM reservation_sum WHERE uuid = $1") + .bind(uuid) + .fetch_one(&pool) + .await?; + let sum_id = summary.id(); + let cust_id = summary.customer; + + Ok(ResSumWithItems{ + summary, + customer: customer_for_reservation(cust_id, &pool).await?, + reservations: items_for_reservation(sum_id, &pool).await? + }) + } + + async fn items_for_reservation(id: i32, pool: &PgPool) -> Result, ServerFnError> { + Ok(query_as::<_, ResWithProperty>( + "SELECT r.id, r.from, r.to, r.property, r.summary, p.name, p,description \ + FROM reservation as r \ + JOIN property as p ON r.property = p.id WHERE r.summary = $1") + .bind(id) + .fetch_all(pool) + .await?) + } + + async fn customer_for_reservation(id: i32, pool: &PgPool) -> Result { + Ok(query_as::<_, Customer>("SELECT * FROM customer WHERE id = $1") + .bind(id) + .fetch_one(pool) + .await?) + } + async fn reservations_in_range(from: &NaiveDate, to: &NaiveDate, state: Option) -> Result, ServerFnError> { let pool = get_pool().await?; let sums = if let Some(s) = state { @@ -74,18 +115,9 @@ cfg_if! { if #[cfg(feature = "ssr")] { return Ok(vec![]) } - let res: Result, Error> = join_all(sums.into_iter().map(|s| async { - let reservations = query_as::<_, ResWithProperty>( - "SELECT r.id, r.from, r.to, r.property, r.summary, p.name, p,description \ - FROM reservation as r \ - JOIN property as p ON r.property = p.id WHERE r.summary = $1") - .bind(s.id()) - .fetch_all(&pool) - .await?; - let customer = query_as::<_, Customer>("SELECT * FROM customer WHERE id = $1") - .bind(s.customer) - .fetch_one(&pool) - .await?; + let res: Result, ServerFnError> = join_all(sums.into_iter().map(|s| async { + let reservations = items_for_reservation(s.id(), &pool).await?; + let customer = customer_for_reservation(s.customer, &pool).await?; Ok(ResSumWithItems { summary: s, customer, @@ -106,6 +138,51 @@ cfg_if! { if #[cfg(feature = "ssr")] { Ok(()) } + + async fn notify_new_all(admin_mail: String, reservation: &ResSumWithItems) -> Result<(), AppError> { + let mailing = get_mailing().await?; + let msg = get_message(MessageType::NewReservation).await?; + + for m in emails_for_notify().await? { + mailing.send_mail(MailMessage::new(admin_mail.clone(), m, msg.clone(), reservation)).await?; + } + + Ok(()) + } + + async fn notify_new(uuid: Uuid) -> Result<(), AppError> { + let mailing = get_mailing().await?; + let msg = get_message(MessageType::NewReservationCust).await?; + let reservation = reservation_by_uuid(uuid).await?; + let admin_mail = admin_email().await; + + if admin_mail.is_none() { + return Err(AppError::MailSendError("No admin mail".to_string())) + } + + mailing.send_mail(MailMessage::new(admin_mail.clone().unwrap(), reservation.customer.email.clone(), msg, &reservation)).await?; + notify_new_all(admin_mail.unwrap(), &reservation).await + } + + async fn send_notify(uuid: Uuid, msg: Message) -> Result<(), AppError> { + let mailing = get_mailing().await?; + let reservation = reservation_by_uuid(uuid).await?; + let admin_mail = admin_email().await; + + if admin_mail.is_none() { + return Err(AppError::MailSendError("No admin mail".to_string())) + } + + mailing.send_mail(MailMessage::new(admin_mail.unwrap(), reservation.customer.email.clone(), msg, &reservation)).await + } + + async fn notify_approve(uuid: Uuid) -> Result<(), AppError> { + send_notify(uuid, get_message(MessageType::ReservationApp).await?).await + } + + async fn notify_cancel(uuid: Uuid) -> Result<(), AppError> { + send_notify(uuid, get_message(MessageType::ReservationCanceled).await?).await + } }} #[server] @@ -206,6 +283,10 @@ pub async fn create_reservation(reservation: CrReservation) -> Result Result, ServerFnError> { use crate::backend::data::ReservationState; perm_check!(is_logged_in); - set_state(Uuid::parse_str(&uuid)?, ReservationState::Approved).await?; + let uuid = Uuid::parse_str(&uuid)?; + set_state(uuid, ReservationState::Approved).await?; + + if let Err(e) = notify_approve(uuid).await { + warn!("Approve notification not send: {}", e); + } Ok(ApiResponse::Data(())) } @@ -258,7 +344,12 @@ pub async fn cancel(uuid: String) -> Result, ServerFnError> { use crate::backend::data::ReservationState; perm_check!(is_logged_in); - set_state(Uuid::parse_str(&uuid)?, ReservationState::Canceled).await?; + let uuid = Uuid::parse_str(&uuid)?; + set_state(uuid, ReservationState::Canceled).await?; + + if let Err(e) = notify_cancel(uuid).await { + warn!("Cancel notification not send: {}", e); + } Ok(ApiResponse::Data(())) } \ No newline at end of file diff --git a/src/backend/user.rs b/src/backend/user.rs index ebfcf57..76271ee 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -10,6 +10,7 @@ cfg_if! { if #[cfg(feature = "ssr")] { use leptos_actix::{extract, redirect}; use log::{info, warn}; use crate::error::AppError; + use crate::backend::get_pool; pub async fn has_admin_user(pool: &PgPool) -> Result { let count: (i64,) = query_as(r#"SELECT COUNT(id) FROM "user" WHERE admin = $1"#) @@ -68,6 +69,36 @@ cfg_if! { if #[cfg(feature = "ssr")] { false } } + + pub async fn admin_email() -> Option { + let pool = get_pool().await.ok()?; + let mail: Result<(String,), Error> = query_as(r#"SELECT email FROM "user" WHERE login = 'admin'"#) + .fetch_one(&pool) + .await; + + if let Ok(m) = mail { + Some(m.0) + } else { + None + } + } + + pub async fn emails_for_notify() -> Result, ServerFnError> { + let pool = get_pool().await?; + let mails: Result, Error> = query_as(r#"SELECT email FROM "user" WHERE (email IS NOT NULL OR email <> '') AND get_emails = true"#) + .fetch_all(&pool) + .await; + + if let Err(e) = mails { + if matches!(e, Error::RowNotFound) { + Ok(vec![]) + } else { + Err(e.into()) + } + } else { + Ok(mails.unwrap().into_iter().map(|m| m.0).collect()) + } + } }} #[server] diff --git a/src/config.rs b/src/config.rs index f55b782..d4abf96 100644 --- a/src/config.rs +++ b/src/config.rs @@ -51,12 +51,58 @@ impl Database { } } +#[cfg(feature = "ssr")] +#[derive(Deserialize, Clone)] +pub enum MailTransport { + Smtp, + File +} + +#[cfg(feature = "ssr")] +#[derive(Deserialize, Clone)] +pub struct Mailing { + transport: MailTransport, + from: String, + path: Option, + server: Option, + user: Option, + password: Option, + tls: Option +} + +#[cfg(feature = "ssr")] +impl Mailing { + pub fn transport(&self) -> &MailTransport { + &self.transport + } + pub fn from(&self) -> &str { + &self.from + } + pub fn path(&self) -> &Option { + &self.path + } + pub fn server(&self) -> &Option { + &self.server + } + pub fn user(&self) -> &Option { + &self.user + } + pub fn password(&self) -> &Option { + &self.password + } + pub fn tls(&self) -> Option { + self.tls + } +} + + #[cfg(feature = "ssr")] #[derive(Deserialize)] pub struct Configuration { session: Session, network: Network, - database: Database + database: Database, + mailing: Mailing } #[cfg(feature = "ssr")] @@ -70,6 +116,9 @@ impl Configuration { pub fn database(&self) -> &Database { &self.database } + pub fn mailing(&self) -> &Mailing { + &self.mailing + } } #[cfg(feature = "ssr")] diff --git a/src/error.rs b/src/error.rs index affcc3c..07efc6b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,7 +8,9 @@ pub enum AppError { HourParseError, ServerError(String), FatalError(String), - SlotParseError + SlotParseError, + MailAddrParseErr(String), + MailSendError(String) } impl AppError { @@ -17,7 +19,9 @@ impl AppError { AppError::HourParseError => {"Hour parse error".to_string()}, AppError::ServerError(e) => {format!("Server error: {}", e)}, AppError::FatalError(e) => {format!("Fatal error: {}", e)}, - AppError::SlotParseError => {"Book slot parse error".to_string()} + AppError::SlotParseError => {"Book slot parse error".to_string()}, + AppError::MailAddrParseErr(e) => {format!("Cannot parse email address: {}", e)}, + AppError::MailSendError(e) => {format!("Cannot send email: {}", e)} } } } @@ -48,3 +52,31 @@ impl From for AppError { AppError::HourParseError } } + +#[cfg(feature = "ssr")] +impl From for AppError { + fn from(value: lettre::address::AddressError) -> Self { + AppError::MailAddrParseErr(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl From for AppError { + fn from(value: lettre::error::Error) -> Self { + AppError::MailSendError(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl From for AppError { + fn from(value: lettre::transport::smtp::Error) -> Self { + AppError::MailSendError(value.to_string()) + } +} + +#[cfg(feature = "ssr")] +impl From for AppError { + fn from(value: lettre::transport::file::Error) -> Self { + AppError::MailSendError(value.to_string()) + } +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 5a8be24..b2e5b16 100644 --- a/src/main.rs +++ b/src/main.rs @@ -51,9 +51,10 @@ async fn main() -> std::io::Result<()> { let pool = PgPoolOptions::new() .max_connections(10) .connect(&srv_conf.database().con_string()).await.unwrap(); - migrate!().run(&pool).await.expect("could not run SQLx migrations"); + let mailing = srv_conf.mailing().clone(); + if let Err(e) = create_admin(&pool).await { error!("Error while checking admin user: {:?}", e); } @@ -69,7 +70,7 @@ async fn main() -> std::io::Result<()> { let site_root = &leptos_options.site_root; App::new() - .app_data(Data::new(AppData::new(pool.clone()))) + .app_data(Data::new(AppData::new(pool.clone(), mailing.clone()))) .wrap(Authentication) .wrap(SessionMiddleware::new( CookieSessionStore::default(),