Add support for sending emails with local Sendmail #6

open
opened by mackuba.eu targeting main from mackuba.eu/pds-gatekeeper: sendmail

I've run into a few problems trying to run Gatekeeper on my PDS. One thing is that I don't use SMTP for sending emails, but a local sendmail service (postfix).

I have it configured like this in pds.env (this is what Nodemailer accepts):

PDS_EMAIL_SMTP_URL=smtp:///?sendmail=true

But Gatekeeper didn't like it:

Error: lettre::transport::smtp::Error { kind: Connection, source: "smtp host undefined" }

I found that lettre supports sendmail, but with a separate SendmailTransport class: https://docs.rs/lettre/latest/lettre/transport/sendmail/index.html

With the help of my good friend GPT, I managed to make it work (the emails are sending) - it added a Mailer wrapper that has a send(msg) method which forwards to either AsyncSmtpTransport or AsyncSendmailTransport. I have no idea if this is how it should be done or how you'd want to do it, but I'm just giving you a starting point :)

Changed files
+341 -19
src
+294 -11
Cargo.lock
··· 84 84 source = "registry+https://github.com/rust-lang/crates.io-index" 85 85 checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" 86 86 87 + [[package]] 88 + name = "async-channel" 89 + version = "1.9.0" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" 92 + dependencies = [ 93 + "concurrent-queue", 94 + "event-listener 2.5.3", 95 + "futures-core", 96 + ] 97 + 98 + [[package]] 99 + name = "async-channel" 100 + version = "2.5.0" 101 + source = "registry+https://github.com/rust-lang/crates.io-index" 102 + checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" 103 + dependencies = [ 104 + "concurrent-queue", 105 + "event-listener-strategy", 106 + "futures-core", 107 + "pin-project-lite", 108 + ] 109 + 87 110 [[package]] 88 111 name = "async-compression" 89 112 version = "0.4.27" ··· 99 122 "zstd-safe", 100 123 ] 101 124 125 + [[package]] 126 + name = "async-executor" 127 + version = "1.13.3" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" 130 + dependencies = [ 131 + "async-task", 132 + "concurrent-queue", 133 + "fastrand", 134 + "futures-lite", 135 + "pin-project-lite", 136 + "slab", 137 + ] 138 + 139 + [[package]] 140 + name = "async-global-executor" 141 + version = "2.4.1" 142 + source = "registry+https://github.com/rust-lang/crates.io-index" 143 + checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" 144 + dependencies = [ 145 + "async-channel 2.5.0", 146 + "async-executor", 147 + "async-io", 148 + "async-lock", 149 + "blocking", 150 + "futures-lite", 151 + "once_cell", 152 + ] 153 + 154 + [[package]] 155 + name = "async-io" 156 + version = "2.6.0" 157 + source = "registry+https://github.com/rust-lang/crates.io-index" 158 + checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" 159 + dependencies = [ 160 + "autocfg", 161 + "cfg-if", 162 + "concurrent-queue", 163 + "futures-io", 164 + "futures-lite", 165 + "parking", 166 + "polling", 167 + "rustix 1.1.2", 168 + "slab", 169 + "windows-sys 0.61.2", 170 + ] 171 + 172 + [[package]] 173 + name = "async-lock" 174 + version = "3.4.1" 175 + source = "registry+https://github.com/rust-lang/crates.io-index" 176 + checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" 177 + dependencies = [ 178 + "event-listener 5.4.1", 179 + "event-listener-strategy", 180 + "pin-project-lite", 181 + ] 182 + 183 + [[package]] 184 + name = "async-process" 185 + version = "2.5.0" 186 + source = "registry+https://github.com/rust-lang/crates.io-index" 187 + checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" 188 + dependencies = [ 189 + "async-channel 2.5.0", 190 + "async-io", 191 + "async-lock", 192 + "async-signal", 193 + "async-task", 194 + "blocking", 195 + "cfg-if", 196 + "event-listener 5.4.1", 197 + "futures-lite", 198 + "rustix 1.1.2", 199 + ] 200 + 201 + [[package]] 202 + name = "async-signal" 203 + version = "0.2.13" 204 + source = "registry+https://github.com/rust-lang/crates.io-index" 205 + checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" 206 + dependencies = [ 207 + "async-io", 208 + "async-lock", 209 + "atomic-waker", 210 + "cfg-if", 211 + "futures-core", 212 + "futures-io", 213 + "rustix 1.1.2", 214 + "signal-hook-registry", 215 + "slab", 216 + "windows-sys 0.61.2", 217 + ] 218 + 219 + [[package]] 220 + name = "async-std" 221 + version = "1.13.2" 222 + source = "registry+https://github.com/rust-lang/crates.io-index" 223 + checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" 224 + dependencies = [ 225 + "async-channel 1.9.0", 226 + "async-global-executor", 227 + "async-io", 228 + "async-lock", 229 + "async-process", 230 + "crossbeam-utils", 231 + "futures-channel", 232 + "futures-core", 233 + "futures-io", 234 + "futures-lite", 235 + "gloo-timers", 236 + "kv-log-macro", 237 + "log", 238 + "memchr", 239 + "once_cell", 240 + "pin-project-lite", 241 + "pin-utils", 242 + "slab", 243 + "wasm-bindgen-futures", 244 + ] 245 + 246 + [[package]] 247 + name = "async-task" 248 + version = "4.7.1" 249 + source = "registry+https://github.com/rust-lang/crates.io-index" 250 + checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" 251 + 102 252 [[package]] 103 253 name = "async-trait" 104 254 version = "0.1.89" ··· 323 473 "generic-array", 324 474 ] 325 475 476 + [[package]] 477 + name = "blocking" 478 + version = "1.6.2" 479 + source = "registry+https://github.com/rust-lang/crates.io-index" 480 + checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" 481 + dependencies = [ 482 + "async-channel 2.5.0", 483 + "async-task", 484 + "futures-io", 485 + "futures-lite", 486 + "piper", 487 + ] 488 + 326 489 [[package]] 327 490 name = "bon" 328 491 version = "3.8.1" ··· 977 1140 "windows-sys 0.48.0", 978 1141 ] 979 1142 1143 + [[package]] 1144 + name = "event-listener" 1145 + version = "2.5.3" 1146 + source = "registry+https://github.com/rust-lang/crates.io-index" 1147 + checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" 1148 + 980 1149 [[package]] 981 1150 name = "event-listener" 982 1151 version = "5.4.1" ··· 988 1157 "pin-project-lite", 989 1158 ] 990 1159 1160 + [[package]] 1161 + name = "event-listener-strategy" 1162 + version = "0.5.4" 1163 + source = "registry+https://github.com/rust-lang/crates.io-index" 1164 + checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" 1165 + dependencies = [ 1166 + "event-listener 5.4.1", 1167 + "pin-project-lite", 1168 + ] 1169 + 991 1170 [[package]] 992 1171 name = "fastrand" 993 1172 version = "2.3.0" ··· 1054 1233 1055 1234 [[package]] 1056 1235 name = "form_urlencoded" 1057 - version = "1.2.1" 1236 + version = "1.2.2" 1058 1237 source = "registry+https://github.com/rust-lang/crates.io-index" 1059 - checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 1238 + checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" 1060 1239 dependencies = [ 1061 1240 "percent-encoding", 1062 1241 ] ··· 1121 1300 source = "registry+https://github.com/rust-lang/crates.io-index" 1122 1301 checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" 1123 1302 1303 + [[package]] 1304 + name = "futures-lite" 1305 + version = "2.6.1" 1306 + source = "registry+https://github.com/rust-lang/crates.io-index" 1307 + checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" 1308 + dependencies = [ 1309 + "fastrand", 1310 + "futures-core", 1311 + "futures-io", 1312 + "parking", 1313 + "pin-project-lite", 1314 + ] 1315 + 1124 1316 [[package]] 1125 1317 name = "futures-macro" 1126 1318 version = "0.3.31" ··· 1230 1422 "regex-syntax 0.8.5", 1231 1423 ] 1232 1424 1425 + [[package]] 1426 + name = "gloo-timers" 1427 + version = "0.3.0" 1428 + source = "registry+https://github.com/rust-lang/crates.io-index" 1429 + checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" 1430 + dependencies = [ 1431 + "futures-channel", 1432 + "futures-core", 1433 + "js-sys", 1434 + "wasm-bindgen", 1435 + ] 1436 + 1233 1437 [[package]] 1234 1438 name = "governor" 1235 1439 version = "0.10.1" ··· 1358 1562 source = "registry+https://github.com/rust-lang/crates.io-index" 1359 1563 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 1360 1564 1565 + [[package]] 1566 + name = "hermit-abi" 1567 + version = "0.5.2" 1568 + source = "registry+https://github.com/rust-lang/crates.io-index" 1569 + checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 1570 + 1361 1571 [[package]] 1362 1572 name = "hex" 1363 1573 version = "0.4.3" ··· 1647 1857 1648 1858 [[package]] 1649 1859 name = "idna" 1650 - version = "1.0.3" 1860 + version = "1.1.0" 1651 1861 source = "registry+https://github.com/rust-lang/crates.io-index" 1652 - checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 1862 + checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" 1653 1863 dependencies = [ 1654 1864 "idna_adapter", 1655 1865 "smallvec", ··· 1962 2172 "sha2", 1963 2173 ] 1964 2174 2175 + [[package]] 2176 + name = "kv-log-macro" 2177 + version = "1.0.7" 2178 + source = "registry+https://github.com/rust-lang/crates.io-index" 2179 + checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" 2180 + dependencies = [ 2181 + "log", 2182 + ] 2183 + 1965 2184 [[package]] 1966 2185 name = "langtag" 1967 2186 version = "0.4.0" ··· 1994 2213 source = "registry+https://github.com/rust-lang/crates.io-index" 1995 2214 checksum = "5cb54db6ff7a89efac87dba5baeac57bb9ccd726b49a9b6f21fb92b3966aaf56" 1996 2215 dependencies = [ 2216 + "async-std", 1997 2217 "async-trait", 1998 2218 "base64", 1999 2219 "chumsky", ··· 2066 2286 source = "registry+https://github.com/rust-lang/crates.io-index" 2067 2287 checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" 2068 2288 2289 + [[package]] 2290 + name = "linux-raw-sys" 2291 + version = "0.11.0" 2292 + source = "registry+https://github.com/rust-lang/crates.io-index" 2293 + checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 2294 + 2069 2295 [[package]] 2070 2296 name = "litemap" 2071 2297 version = "0.8.0" ··· 2087 2313 version = "0.4.27" 2088 2314 source = "registry+https://github.com/rust-lang/crates.io-index" 2089 2315 checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 2316 + dependencies = [ 2317 + "value-bag", 2318 + ] 2090 2319 2091 2320 [[package]] 2092 2321 name = "lru-slab" ··· 2502 2731 "tower_governor", 2503 2732 "tracing", 2504 2733 "tracing-subscriber", 2734 + "url", 2505 2735 "urlencoding", 2506 2736 ] 2507 2737 ··· 2516 2746 2517 2747 [[package]] 2518 2748 name = "percent-encoding" 2519 - version = "2.3.1" 2749 + version = "2.3.2" 2520 2750 source = "registry+https://github.com/rust-lang/crates.io-index" 2521 - checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 2751 + checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 2522 2752 2523 2753 [[package]] 2524 2754 name = "pest" ··· 2596 2826 source = "registry+https://github.com/rust-lang/crates.io-index" 2597 2827 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 2598 2828 2829 + [[package]] 2830 + name = "piper" 2831 + version = "0.2.4" 2832 + source = "registry+https://github.com/rust-lang/crates.io-index" 2833 + checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" 2834 + dependencies = [ 2835 + "atomic-waker", 2836 + "fastrand", 2837 + "futures-io", 2838 + ] 2839 + 2599 2840 [[package]] 2600 2841 name = "pkcs1" 2601 2842 version = "0.7.5" ··· 2623 2864 source = "registry+https://github.com/rust-lang/crates.io-index" 2624 2865 checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 2625 2866 2867 + [[package]] 2868 + name = "polling" 2869 + version = "3.11.0" 2870 + source = "registry+https://github.com/rust-lang/crates.io-index" 2871 + checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" 2872 + dependencies = [ 2873 + "cfg-if", 2874 + "concurrent-queue", 2875 + "hermit-abi", 2876 + "pin-project-lite", 2877 + "rustix 1.1.2", 2878 + "windows-sys 0.61.2", 2879 + ] 2880 + 2626 2881 [[package]] 2627 2882 name = "portable-atomic" 2628 2883 version = "1.11.1" ··· 3122 3377 "bitflags", 3123 3378 "errno", 3124 3379 "libc", 3125 - "linux-raw-sys", 3380 + "linux-raw-sys 0.4.15", 3381 + "windows-sys 0.59.0", 3382 + ] 3383 + 3384 + [[package]] 3385 + name = "rustix" 3386 + version = "1.1.2" 3387 + source = "registry+https://github.com/rust-lang/crates.io-index" 3388 + checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 3389 + dependencies = [ 3390 + "bitflags", 3391 + "errno", 3392 + "libc", 3393 + "linux-raw-sys 0.11.0", 3126 3394 "windows-sys 0.59.0", 3127 3395 ] 3128 3396 ··· 3561 3829 "crc", 3562 3830 "crossbeam-queue", 3563 3831 "either", 3564 - "event-listener", 3832 + "event-listener 5.4.1", 3565 3833 "futures-core", 3566 3834 "futures-intrusive", 3567 3835 "futures-io", ··· 4279 4547 4280 4548 [[package]] 4281 4549 name = "url" 4282 - version = "2.5.4" 4550 + version = "2.5.7" 4283 4551 source = "registry+https://github.com/rust-lang/crates.io-index" 4284 - checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 4552 + checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" 4285 4553 dependencies = [ 4286 4554 "form_urlencoded", 4287 4555 "idna", ··· 4313 4581 source = "registry+https://github.com/rust-lang/crates.io-index" 4314 4582 checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" 4315 4583 4584 + [[package]] 4585 + name = "value-bag" 4586 + version = "1.12.0" 4587 + source = "registry+https://github.com/rust-lang/crates.io-index" 4588 + checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" 4589 + 4316 4590 [[package]] 4317 4591 name = "vcpkg" 4318 4592 version = "0.2.15" ··· 4496 4770 "either", 4497 4771 "home", 4498 4772 "once_cell", 4499 - "rustix", 4773 + "rustix 0.38.44", 4500 4774 ] 4501 4775 4502 4776 [[package]] ··· 4643 4917 "windows-targets 0.52.6", 4644 4918 ] 4645 4919 4920 + [[package]] 4921 + name = "windows-sys" 4922 + version = "0.61.2" 4923 + source = "registry+https://github.com/rust-lang/crates.io-index" 4924 + checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 4925 + dependencies = [ 4926 + "windows-link 0.2.1", 4927 + ] 4928 + 4646 4929 [[package]] 4647 4930 name = "windows-targets" 4648 4931 version = "0.48.5"
+2 -1
Cargo.toml
··· 22 22 #Leaveing these two cause I think it is needed by the email crate for ssl 23 23 aws-lc-rs = "1.13.0" 24 24 rustls = { version = "0.23", default-features = false, features = ["tls12", "std", "logging", "aws_lc_rs"] } 25 - lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "tokio1", "tokio1-rustls"] } 25 + lettre = { version = "0.11", default-features = false, features = ["builder", "webpki-roots", "rustls", "aws-lc-rs", "smtp-transport", "sendmail-transport", "tokio1", "tokio1-rustls"] } 26 26 handlebars = { version = "6.3.2", features = ["rust-embed"] } 27 27 rust-embed = "8.7.2" 28 28 axum-template = { version = "3.0.0", features = ["handlebars"] } ··· 35 35 multibase = "0.9.2" 36 36 reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } 37 37 urlencoding = "2.1" 38 + url = "2.5.7" 38 39 html-escape = "0.2.13" 39 40 josekit = "0.10.3"
+1 -1
src/helpers.rs
··· 17 17 use jacquard_identity::{PublicResolver, resolver::IdentityResolver}; 18 18 use josekit::jwe::alg::direct::DirectJweAlgorithm; 19 19 use lettre::{ 20 - AsyncTransport, Message, 20 + Message, 21 21 message::{MultiPart, SinglePart, header}, 22 22 }; 23 23 use rand::Rng;
+44 -6
src/main.rs
··· 4 4 use crate::xrpc::com_atproto_server::{ 5 5 create_account, create_session, describe_server, get_session, update_email, 6 6 }; 7 + use anyhow::{Result, Context}; 7 8 use axum::{ 8 9 Router, 9 10 body::Body, ··· 18 19 use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor}; 19 20 use jacquard_common::types::did::Did; 20 21 use jacquard_identity::{PublicResolver, resolver::PlcSource}; 21 - use lettre::{AsyncSmtpTransport, Tokio1Executor}; 22 + use lettre::{AsyncTransport, AsyncSmtpTransport, AsyncSendmailTransport, Message, Tokio1Executor}; 22 23 use rand::Rng; 23 24 use rust_embed::RustEmbed; 24 25 use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; ··· 36 37 }; 37 38 use tracing::log; 38 39 use tracing_subscriber::{EnvFilter, fmt, prelude::*}; 40 + use url::Url; 39 41 40 42 mod gate; 41 43 pub mod helpers; ··· 150 152 account_pool: SqlitePool, 151 153 pds_gatekeeper_pool: SqlitePool, 152 154 reverse_proxy_client: HyperUtilClient, 153 - mailer: AsyncSmtpTransport<Tokio1Executor>, 155 + mailer: Arc<Mailer>, 154 156 template_engine: Engine<Handlebars<'static>>, 155 157 resolver: Arc<PublicResolver>, 156 158 app_config: AppConfig, 157 159 } 158 160 161 + pub enum Mailer { 162 + Smtp(AsyncSmtpTransport<Tokio1Executor>), 163 + Sendmail(AsyncSendmailTransport<Tokio1Executor>), 164 + } 165 + 166 + impl Mailer { 167 + pub async fn send(&self, msg: Message) -> Result<()> { 168 + match self { 169 + Mailer::Smtp(m) => { 170 + m.send(msg).await.context("SMTP send failed")?; 171 + Ok(()) 172 + } 173 + Mailer::Sendmail(m) => { 174 + m.send(msg).await.context("sendmail send failed")?; 175 + Ok(()) 176 + } 177 + } 178 + } 179 + } 180 + 181 + fn build_mailer_from_env() -> Result<Mailer> { 182 + let raw = env::var("PDS_EMAIL_SMTP_URL") 183 + .context("PDS_EMAIL_SMTP_URL is not set in your pds.env file")?; 184 + 185 + let url = Url::parse(&raw).context("PDS_EMAIL_SMTP_URL is not a valid URL")?; 186 + 187 + let use_sendmail = url.scheme() == "sendmail" 188 + || url.query_pairs().any(|(k, v)| k == "sendmail" && v == "true"); 189 + 190 + if use_sendmail { 191 + Ok(Mailer::Sendmail(AsyncSendmailTransport::<Tokio1Executor>::new())) 192 + } else { 193 + Ok(Mailer::Smtp( 194 + AsyncSmtpTransport::<Tokio1Executor>::from_url(raw.as_str())? 195 + .build(), 196 + )) 197 + } 198 + } 199 + 159 200 async fn root_handler() -> impl axum::response::IntoResponse { 160 201 let body = r" 161 202 ··· 242 283 .build(HttpConnector::new()); 243 284 244 285 //Emailer set up 245 - let smtp_url = 246 - env::var("PDS_EMAIL_SMTP_URL").expect("PDS_EMAIL_SMTP_URL is not set in your pds.env file"); 286 + let mailer = Arc::new(build_mailer_from_env()?); 247 287 248 - let mailer: AsyncSmtpTransport<Tokio1Executor> = 249 - AsyncSmtpTransport::<Tokio1Executor>::from_url(smtp_url.as_str())?.build(); 250 288 //Email templates setup 251 289 let mut hbs = Handlebars::new(); 252 290