diff options
-rw-r--r-- | Cargo.lock | 498 | ||||
-rw-r--r-- | Cargo.toml | 8 | ||||
-rw-r--r-- | README.md | 68 | ||||
-rw-r--r-- | rust-toolchain.toml | 3 | ||||
-rw-r--r-- | src/.server.rs.swo | bin | 0 -> 12288 bytes | |||
-rw-r--r-- | src/.service.rs.swo | bin | 0 -> 12288 bytes | |||
-rw-r--r-- | src/.session.rs.swo | bin | 0 -> 20480 bytes | |||
-rw-r--r-- | src/command.rs | 112 | ||||
-rw-r--r-- | src/config.rs | 12 | ||||
-rw-r--r-- | src/lmtp.rs | 4 | ||||
-rw-r--r-- | src/login/static_provider.rs | 6 | ||||
-rw-r--r-- | src/mail_ident.rs (renamed from src/mail_uuid.rs) | 38 | ||||
-rw-r--r-- | src/mailbox.rs | 52 | ||||
-rw-r--r-- | src/mailstore.rs | 33 | ||||
-rw-r--r-- | src/main.rs | 17 | ||||
-rw-r--r-- | src/server.rs | 93 | ||||
-rw-r--r-- | src/service.rs | 62 | ||||
-rw-r--r-- | src/session.rs | 145 | ||||
-rw-r--r-- | src/uidindex.rs | 281 |
19 files changed, 1224 insertions, 208 deletions
@@ -3,6 +3,21 @@ version = 3 [[package]] +name = "abnf-core" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871a574ed52e84ec15e6266d57d477e3e5c396cd86f9b05f2cb629a2c5af2eec" +dependencies = [ + "nom 6.1.2", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] name = "aho-corasick" version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -12,6 +27,15 @@ dependencies = [ ] [[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] name = "anyhow" version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -46,6 +70,19 @@ dependencies = [ ] [[package]] +name = "async-compat" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b48b4ff0c2026db683dea961cd8ea874737f56cffca86fa84415eaddc51c00d" +dependencies = [ + "futures-core", + "futures-io", + "once_cell", + "pin-project-lite 0.2.9", + "tokio", +] + +[[package]] name = "async-executor" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -176,9 +213,9 @@ checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -323,10 +360,29 @@ dependencies = [ ] [[package]] +name = "boitalettres" +version = "0.0.1" +source = "git+https://git.deuxfleurs.fr/KokaKiwi/boitalettres.git?branch=main#fc5f09356466d51404317c1b09e19720dd50c314" +dependencies = [ + "async-compat", + "bytes", + "futures", + "imap-codec", + "miette", + "pin-project", + "thiserror", + "tokio", + "tokio-tower", + "tower", + "tracing", + "tracing-futures", +] + +[[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" @@ -377,16 +433,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "6d20de3739b4fb45a17837824f40aa1769cc7655d7a83e68739a77fe7b30c87a" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap", @@ -394,9 +450,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "026baf08b89ffbd332836002ec9378ef0e69648cbfadd68af7cd398ca5bf98f7" dependencies = [ "heck", "proc-macro-error", @@ -407,9 +463,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" dependencies = [ "os_str_bytes", ] @@ -458,6 +514,65 @@ dependencies = [ ] [[package]] +name = "crossbeam" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "lazy_static", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] name = "crossbeam-utils" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -603,6 +718,16 @@ dependencies = [ ] [[package]] +name = "flate2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] name = "fnv" version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -755,13 +880,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -802,6 +927,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] +name = "hdrhistogram" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31672b7011be2c4f7456c4ddbcb40e7e9a4a9fad8efe49a6ebaf5f307d0109c0" +dependencies = [ + "base64", + "byteorder", + "crossbeam-channel", + "flate2", + "nom 7.1.1", + "num-traits", +] + +[[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -834,9 +973,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -877,9 +1016,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -938,10 +1077,23 @@ dependencies = [ ] [[package]] +name = "imap-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab1edebd5f2288f8c195ae53fbb7342f5e568739b439a5923be21a9f61f3364" +dependencies = [ + "abnf-core", + "base64", + "chrono", + "nom 6.1.2", + "rand", +] + +[[package]] name = "indexmap" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg", "hashbrown", @@ -982,9 +1134,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -992,7 +1144,7 @@ dependencies = [ [[package]] name = "k2v-client" version = "0.1.0" -source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?branch=improve-k2v-client#a73f174ada005f71a77f12e185da154aa5c254a9" +source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?branch=main#d544a0e0e03c9b69b226fb5bba2ce27a7af270ca" dependencies = [ "base64", "http", @@ -1088,6 +1240,16 @@ dependencies = [ ] [[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] name = "log" version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1105,11 +1267,13 @@ dependencies = [ "argon2", "async-trait", "base64", + "boitalettres", "clap", "duplexify", "futures", "hex", "im", + "imap-codec", "itertools", "k2v-client", "lazy_static", @@ -1130,6 +1294,9 @@ dependencies = [ "tokio", "tokio-util", "toml", + "tower", + "tracing", + "tracing-subscriber", "zstd", ] @@ -1157,6 +1324,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miette" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c" +dependencies = [ + "miette-derive", + "once_cell", + "thiserror", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +dependencies = [ + "adler", +] + +[[package]] name = "mio" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1206,6 +1420,16 @@ dependencies = [ ] [[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] name = "num-integer" version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1280,9 +1504,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.73" +version = "0.9.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" dependencies = [ "autocfg", "cc", @@ -1293,9 +1517,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.0.1" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "029d8d0b2f198229de29dca79676f2738ff952edf3fde542eb8bf94d8c21b435" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "parking" @@ -1304,6 +1528,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" [[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] name = "password-hash" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1698,6 +1945,12 @@ dependencies = [ ] [[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] name = "security-framework" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1722,9 +1975,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" [[package]] name = "serde" @@ -1771,6 +2024,15 @@ dependencies = [ ] [[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1818,6 +2080,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] name = "smol" version = "1.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1915,9 +2183,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" dependencies = [ "proc-macro2", "quote", @@ -1980,12 +2248,22 @@ dependencies = [ ] [[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -2006,9 +2284,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.18.2" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ "bytes", "libc", @@ -2016,6 +2294,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", + "parking_lot", "pin-project-lite 0.2.9", "signal-hook-registry", "socket2", @@ -2025,9 +2304,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -2046,9 +2325,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" dependencies = [ "futures-core", "pin-project-lite 0.2.9", @@ -2056,10 +2335,27 @@ dependencies = [ ] [[package]] +name = "tokio-tower" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4322b6e2ebfd3be4082c16df4341505ef333683158b55f22afaf3f61565d728" +dependencies = [ + "crossbeam", + "futures-core", + "futures-sink", + "futures-util", + "pin-project", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] name = "tokio-util" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -2080,6 +2376,33 @@ dependencies = [ ] [[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "hdrhistogram", + "indexmap", + "pin-project", + "pin-project-lite 0.2.9", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] name = "tower-service" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2087,11 +2410,12 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", + "log", "pin-project-lite 0.2.9", "tracing-attributes", "tracing-core", @@ -2110,11 +2434,47 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" dependencies = [ "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "ansi_term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2137,9 +2497,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" @@ -2151,6 +2511,12 @@ dependencies = [ ] [[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] name = "url" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2163,6 +2529,12 @@ dependencies = [ ] [[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] name = "value-bag" version = "1.0.0-alpha.9" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2213,9 +2585,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" @@ -2225,9 +2597,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2235,9 +2607,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" dependencies = [ "bumpalo", "lazy_static", @@ -2250,9 +2622,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" dependencies = [ "cfg-if", "js-sys", @@ -2262,9 +2634,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2272,9 +2644,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", @@ -2285,15 +2657,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" dependencies = [ "js-sys", "wasm-bindgen", @@ -35,8 +35,14 @@ tokio-util = { version = "0.7", features = [ "compat" ] } toml = "0.5" zstd = { version = "0.9", default-features = false } +tracing-subscriber = "0.3" +tracing = "0.1" +tower = "0.4" +imap-codec = "0.5" + +k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "main" } +boitalettres = { git = "https://git.deuxfleurs.fr/KokaKiwi/boitalettres.git", branch = "main" } smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" } -k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "improve-k2v-client" } #k2v-client = { path = "../garage/src/k2v-client" } @@ -1,5 +1,73 @@ # Mailrage - Encrypted e-mail storage over Garage +## Usage + +Start by running: + +``` +$ cargo run --bin main -- first-login --region garage --k2v-endpoint http://127.0.0.1:3904 --s3-endpoint http://127.0.0.1:3900 --aws-access-key-id GK... --aws-secret-access-key c0ffee... --bucket mailrage-quentin --user-secret poupou +Please enter your password for key decryption. +If you are using LDAP login, this must be your LDAP password. +If you are using the static login provider, enter any password, and this will also become your password for local IMAP access. +Enter password: +Confirm password: + +Cryptographic key setup is complete. + +If you are using the static login provider, add the following section to your .toml configuration file: + +[login_static.users.<username>] +password = "$argon2id$v=19$m=4096,t=3,p=1$..." +aws_access_key_id = "GK..." +aws_secret_access_key = "c0ffee..." +``` + +Next create the config file `mailrage.toml`: + +``` +s3_endpoint = "http://127.0.0.1:3900" +k2v_endpoint = "http://127.0.0.1:3904" +aws_region = "garage" + +[login_static] +default_bucket = "mailrage" +[login_static.users.quentin] +bucket = "mailrage-quentin" +user_secret = "poupou" +alternate_user_secrets = [] +password = "$argon2id$v=19$m=4096,t=3,p=1$..." +aws_access_key_id = "GK..." +aws_secret_access_key = "c0ffee..." +``` + +You can dump your keys with: + +``` +$ cargo run --bin main -- show-keys --region garage --k2v-endpoint http://127.0.0.1:3904 --s3-endpoint http://127.0.0.1:3900 --aws-access-key-id GK... --aws-secret-access-key c0ffee... --bucket mailrage-quentin --user-secret poupou +Enter key decryption password: +master_key = "..." +secret_key = "..." +``` + +Run a test instance with: + +``` +$ cargo run --bin main -- server +---- MAILBOX STATE ---- +UIDVALIDITY 1 +UIDNEXT 2 +INTERNALSEQ 2 +1 c3d4524f557f19108480063f3216afa20000000000000000 \Unseen + +---- MAILBOX STATE ---- +UIDVALIDITY 1 +UIDNEXT 3 +INTERNALSEQ 3 +1 c3d4524f557f19108480063f3216afa20000000000000000 \Unseen +2 6a1ab4d87af3d424a3a8f8720c4db3b60000000000000000 \Unseen +``` + + ## Bayou storage module Checkpoints are stored in S3 at `<path>/checkpoint/<timestamp>`. Example: diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..f960780 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2022-06-14" +components = ["rustc-dev", "rust-src"] diff --git a/src/.server.rs.swo b/src/.server.rs.swo Binary files differnew file mode 100644 index 0000000..9e99bb3 --- /dev/null +++ b/src/.server.rs.swo diff --git a/src/.service.rs.swo b/src/.service.rs.swo Binary files differnew file mode 100644 index 0000000..a69e975 --- /dev/null +++ b/src/.service.rs.swo diff --git a/src/.session.rs.swo b/src/.session.rs.swo Binary files differnew file mode 100644 index 0000000..a6de20e --- /dev/null +++ b/src/.session.rs.swo diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..4a2723d --- /dev/null +++ b/src/command.rs @@ -0,0 +1,112 @@ +use anyhow::Result; +use boitalettres::errors::Error as BalError; +use boitalettres::proto::{Request, Response}; +use imap_codec::types::core::{AString, Tag}; +use imap_codec::types::fetch_attributes::MacroOrFetchAttributes; +use imap_codec::types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; +use imap_codec::types::response::{Capability, Data}; +use imap_codec::types::sequence::SequenceSet; + +use crate::mailbox::Mailbox; +use crate::session; + +pub struct Command<'a> { + tag: Tag, + session: &'a mut session::Instance, +} + +impl<'a> Command<'a> { + pub fn new(tag: Tag, session: &'a mut session::Instance) -> Self { + Self { tag, session } + } + + pub async fn capability(&self) -> Result<Response> { + let capabilities = vec![Capability::Imap4Rev1, Capability::Idle]; + let body = vec![Data::Capability(capabilities)]; + let r = Response::ok("Pre-login capabilities listed, post-login capabilities have more.")? + .with_body(body); + Ok(r) + } + + pub async fn login(&mut self, username: AString, password: AString) -> Result<Response> { + let (u, p) = (String::try_from(username)?, String::try_from(password)?); + tracing::info!(user = %u, "command.login"); + + let creds = match self.session.login_provider.login(&u, &p).await { + Err(_) => { + return Ok(Response::no( + "[AUTHENTICATIONFAILED] Authentication failed.", + )?) + } + Ok(c) => c, + }; + + self.session.user = Some(session::User { + creds, + name: u.clone(), + }); + + tracing::info!(username=%u, "connected"); + Ok(Response::ok("Logged in")?) + } + + pub async fn lsub( + &self, + reference: MailboxCodec, + mailbox_wildcard: ListMailbox, + ) -> Result<Response> { + Ok(Response::bad("Not implemented")?) + } + + pub async fn list( + &self, + reference: MailboxCodec, + mailbox_wildcard: ListMailbox, + ) -> Result<Response> { + Ok(Response::bad("Not implemented")?) + } + + /* + * TRACE BEGIN --- + + + Example: C: A142 SELECT INBOX + S: * 172 EXISTS + S: * 1 RECENT + S: * OK [UNSEEN 12] Message 12 is first unseen + S: * OK [UIDVALIDITY 3857529045] UIDs valid + S: * OK [UIDNEXT 4392] Predicted next UID + S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) + S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited + S: A142 OK [READ-WRITE] SELECT completed + + * TRACE END --- + */ + pub async fn select(&mut self, mailbox: MailboxCodec) -> Result<Response> { + let name = String::try_from(mailbox)?; + let user = match self.session.user.as_ref() { + Some(u) => u, + _ => return Ok(Response::no("You must be connected to use SELECT")?), + }; + + let mut mb = Mailbox::new(&user.creds, name.clone())?; + tracing::info!(username=%user.name, mailbox=%name, "mailbox.selected"); + + let sum = mb.summary().await?; + tracing::trace!(summary=%sum, "mailbox.summary"); + + let body = vec![Data::Exists(sum.exists.try_into()?), Data::Recent(0)]; + + self.session.selected = Some(mb); + Ok(Response::ok("[READ-WRITE] Select completed")?.with_body(body)) + } + + pub async fn fetch( + &self, + sequence_set: SequenceSet, + attributes: MacroOrFetchAttributes, + uid: bool, + ) -> Result<Response> { + Ok(Response::bad("Not implemented")?) + } +} diff --git a/src/config.rs b/src/config.rs index 9ec0ea1..5afcabd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,9 +4,9 @@ use std::net::SocketAddr; use std::path::PathBuf; use anyhow::Result; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Config { pub s3_endpoint: String, pub k2v_endpoint: String, @@ -18,13 +18,13 @@ pub struct Config { pub lmtp: Option<LmtpConfig>, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct LoginStaticConfig { pub default_bucket: Option<String>, pub users: HashMap<String, LoginStaticUser>, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct LoginStaticUser { #[serde(default)] pub email_addresses: Vec<String>, @@ -42,7 +42,7 @@ pub struct LoginStaticUser { pub secret_key: Option<String>, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct LoginLdapConfig { pub ldap_server: String, @@ -65,7 +65,7 @@ pub struct LoginLdapConfig { pub bucket_attr: Option<String>, } -#[derive(Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct LmtpConfig { pub bind_addr: SocketAddr, pub hostname: String, diff --git a/src/lmtp.rs b/src/lmtp.rs index 4186d69..049e119 100644 --- a/src/lmtp.rs +++ b/src/lmtp.rs @@ -20,7 +20,7 @@ use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata, Pro use crate::config::*; use crate::cryptoblob::*; use crate::login::*; -use crate::mail_uuid::*; +use crate::mail_ident::*; pub struct LmtpServer { bind_addr: SocketAddr, @@ -249,7 +249,7 @@ impl EncryptedMessage { let mut por = PutObjectRequest::default(); por.bucket = creds.storage.bucket.clone(); - por.key = format!("incoming/{}", gen_uuid().to_string()); + por.key = format!("incoming/{}", gen_ident().to_string()); por.metadata = Some( [("Message-Key".to_string(), key_header)] .into_iter() diff --git a/src/login/static_provider.rs b/src/login/static_provider.rs index aa5e499..6bbc717 100644 --- a/src/login/static_provider.rs +++ b/src/login/static_provider.rs @@ -48,14 +48,18 @@ impl StaticLoginProvider { #[async_trait] impl LoginProvider for StaticLoginProvider { async fn login(&self, username: &str, password: &str) -> Result<Credentials> { + tracing::debug!(user=%username, "login"); let user = match self.users.get(username) { None => bail!("User {} does not exist", username), Some(u) => u, }; + tracing::debug!(user=%username, "verify password"); if !verify_password(password, &user.password)? { bail!("Wrong password"); } + + tracing::debug!(user=%username, "fetch bucket"); let bucket = user .bucket .clone() @@ -64,6 +68,7 @@ impl LoginProvider for StaticLoginProvider { "No bucket configured and no default bucket specieid" ))?; + tracing::debug!(user=%username, "fetch keys"); let storage = StorageCredentials { k2v_region: self.k2v_region.clone(), s3_region: self.s3_region.clone(), @@ -92,6 +97,7 @@ impl LoginProvider for StaticLoginProvider { ), }; + tracing::debug!(user=%username, "logged"); Ok(Credentials { storage, keys }) } diff --git a/src/mail_uuid.rs b/src/mail_ident.rs index ab76bce..07e053a 100644 --- a/src/mail_uuid.rs +++ b/src/mail_ident.rs @@ -7,20 +7,24 @@ use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use crate::time::now_msec; -/// A Mail UUID is composed of two components: +/// An internal Mail Identifier is composed of two components: /// - a process identifier, 128 bits, itself composed of: /// - the timestamp of when the process started, 64 bits /// - a 64-bit random number /// - a sequence number, 64 bits -#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Debug)] -pub struct MailUuid(pub [u8; 24]); - -struct UuidGenerator { +/// They are not part of the protocol but an internal representation +/// required by Mailrage/Aerogramme. +/// Their main property is to be unique without having to rely +/// on synchronization between IMAP processes. +#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, Debug)] +pub struct MailIdent(pub [u8; 24]); + +struct IdentGenerator { pid: u128, sn: AtomicU64, } -impl UuidGenerator { +impl IdentGenerator { fn new() -> Self { let time = now_msec() as u128; let rand = thread_rng().gen::<u64>() as u128; @@ -30,36 +34,36 @@ impl UuidGenerator { } } - fn gen(&self) -> MailUuid { + fn gen(&self) -> MailIdent { let sn = self.sn.fetch_add(1, Ordering::Relaxed); let mut res = [0u8; 24]; res[0..16].copy_from_slice(&u128::to_be_bytes(self.pid)); res[16..24].copy_from_slice(&u64::to_be_bytes(sn)); - MailUuid(res) + MailIdent(res) } } lazy_static! { - static ref GENERATOR: UuidGenerator = UuidGenerator::new(); + static ref GENERATOR: IdentGenerator = IdentGenerator::new(); } -pub fn gen_uuid() -> MailUuid { +pub fn gen_ident() -> MailIdent { GENERATOR.gen() } // -- serde -- -impl<'de> Deserialize<'de> for MailUuid { +impl<'de> Deserialize<'de> for MailIdent { fn deserialize<D>(d: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let v = String::deserialize(d)?; - MailUuid::from_str(&v).map_err(D::Error::custom) + MailIdent::from_str(&v).map_err(D::Error::custom) } } -impl Serialize for MailUuid { +impl Serialize for MailIdent { fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer, @@ -68,16 +72,16 @@ impl Serialize for MailUuid { } } -impl ToString for MailUuid { +impl ToString for MailIdent { fn to_string(&self) -> String { hex::encode(self.0) } } -impl FromStr for MailUuid { +impl FromStr for MailIdent { type Err = &'static str; - fn from_str(s: &str) -> Result<MailUuid, &'static str> { + fn from_str(s: &str) -> Result<MailIdent, &'static str> { let bytes = hex::decode(s).map_err(|_| "invalid hex")?; if bytes.len() != 24 { @@ -86,6 +90,6 @@ impl FromStr for MailUuid { let mut tmp = [0u8; 24]; tmp[..].copy_from_slice(&bytes); - Ok(MailUuid(tmp)) + Ok(MailIdent(tmp)) } } diff --git a/src/mailbox.rs b/src/mailbox.rs index 49d8e56..249d329 100644 --- a/src/mailbox.rs +++ b/src/mailbox.rs @@ -5,12 +5,27 @@ use rusoto_s3::S3Client; use crate::bayou::Bayou; use crate::cryptoblob::Key; use crate::login::Credentials; -use crate::mail_uuid::*; +use crate::mail_ident::*; use crate::uidindex::*; +pub struct Summary { + pub validity: ImapUidvalidity, + pub next: ImapUid, + pub exists: usize, +} +impl std::fmt::Display for Summary { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "uidvalidity: {}, uidnext: {}, exists: {}", + self.validity, self.next, self.exists + ) + } +} + pub struct Mailbox { bucket: String, - name: String, + pub name: String, key: Key, k2v: K2vClient, @@ -20,7 +35,7 @@ pub struct Mailbox { } impl Mailbox { - pub async fn new(creds: &Credentials, name: String) -> Result<Self> { + pub fn new(creds: &Credentials, name: String) -> Result<Self> { let uid_index = Bayou::<UidIndex>::new(creds, name.clone())?; Ok(Self { @@ -33,6 +48,17 @@ impl Mailbox { }) } + pub async fn summary(&mut self) -> Result<Summary> { + self.uid_index.sync().await?; + let state = self.uid_index.state(); + + return Ok(Summary { + validity: state.uidvalidity, + next: state.uidnext, + exists: state.idx_by_uid.len(), + }); + } + pub async fn test(&mut self) -> Result<()> { self.uid_index.sync().await?; @@ -41,22 +67,22 @@ impl Mailbox { let add_mail_op = self .uid_index .state() - .op_mail_add(gen_uuid(), vec!["\\Unseen".into()]); + .op_mail_add(gen_ident(), vec!["\\Unseen".into()]); self.uid_index.push(add_mail_op).await?; dump(&self.uid_index); - if self.uid_index.state().mails_by_uid.len() > 6 { + if self.uid_index.state().idx_by_uid.len() > 6 { for i in 0..2 { - let (_, uuid) = self + let (_, ident) = self .uid_index .state() - .mails_by_uid + .idx_by_uid .iter() .skip(3 + i) .next() .unwrap(); - let del_mail_op = self.uid_index.state().op_mail_del(*uuid); + let del_mail_op = self.uid_index.state().op_mail_del(*ident); self.uid_index.push(del_mail_op).await?; dump(&self.uid_index); @@ -73,16 +99,12 @@ fn dump(uid_index: &Bayou<UidIndex>) { println!("UIDVALIDITY {}", s.uidvalidity); println!("UIDNEXT {}", s.uidnext); println!("INTERNALSEQ {}", s.internalseq); - for (uid, uuid) in s.mails_by_uid.iter() { + for (uid, ident) in s.idx_by_uid.iter() { println!( "{} {} {}", uid, - hex::encode(uuid.0), - s.mail_flags - .get(uuid) - .cloned() - .unwrap_or_default() - .join(", ") + hex::encode(ident.0), + s.table.get(ident).cloned().unwrap_or_default().1.join(", ") ); } println!(""); diff --git a/src/mailstore.rs b/src/mailstore.rs new file mode 100644 index 0000000..2bcc592 --- /dev/null +++ b/src/mailstore.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use anyhow::{bail, Result}; +use rusoto_signature::Region; + +use crate::config::*; +use crate::login::{ldap_provider::*, static_provider::*, *}; + +pub struct Mailstore { + pub login_provider: Box<dyn LoginProvider + Send + Sync>, +} +impl Mailstore { + pub fn new(config: Config) -> Result<Arc<Self>> { + let s3_region = Region::Custom { + name: config.aws_region.clone(), + endpoint: config.s3_endpoint, + }; + let k2v_region = Region::Custom { + name: config.aws_region, + endpoint: config.k2v_endpoint, + }; + let login_provider: Box<dyn LoginProvider + Send + Sync> = + match (config.login_static, config.login_ldap) { + (Some(st), None) => Box::new(StaticLoginProvider::new(st, k2v_region, s3_region)?), + (None, Some(ld)) => Box::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?), + (Some(_), Some(_)) => { + bail!("A single login provider must be set up in config file") + } + (None, None) => bail!("No login provider is set up in config file"), + }; + Ok(Arc::new(Self { login_provider })) + } +} diff --git a/src/main.rs b/src/main.rs index 33d3188..9ec5af0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,15 @@ mod bayou; +mod command; mod config; mod cryptoblob; mod lmtp; mod login; -mod mail_uuid; +mod mail_ident; mod mailbox; +mod mailstore; mod server; +mod service; +mod session; mod time; mod uidindex; @@ -118,9 +122,10 @@ struct UserSecretsArgs { #[tokio::main] async fn main() -> Result<()> { if std::env::var("RUST_LOG").is_err() { - std::env::set_var("RUST_LOG", "mailrage=info,k2v_client=info") + std::env::set_var("RUST_LOG", "main=info,mailrage=info,k2v_client=info") } - pretty_env_logger::init(); + + tracing_subscriber::fmt::init(); let args = Args::parse(); @@ -128,14 +133,14 @@ async fn main() -> Result<()> { Command::Server { config_file } => { let config = read_config(config_file)?; - let server = Server::new(config)?; + let server = Server::new(config).await?; server.run().await?; } Command::Test { config_file } => { let config = read_config(config_file)?; - let server = Server::new(config)?; - server.test().await?; + let server = Server::new(config).await?; + //server.test().await?; } Command::FirstLogin { creds, diff --git a/src/server.rs b/src/server.rs index 1fd21b4..3abdfd1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,78 +1,97 @@ use std::sync::Arc; + + +use boitalettres::server::accept::addr::AddrIncoming; +use boitalettres::server::accept::addr::AddrStream; +use boitalettres::server::Server as ImapServer; + use anyhow::{bail, Result}; use futures::{try_join, StreamExt}; use log::*; use rusoto_signature::Region; use tokio::sync::watch; +use tower::Service; -use crate::config::*; +use crate::mailstore; +use crate::service; use crate::lmtp::*; +use crate::config::*; use crate::login::{ldap_provider::*, static_provider::*, *}; use crate::mailbox::Mailbox; pub struct Server { - pub login_provider: Arc<dyn LoginProvider + Send + Sync>, - pub lmtp_server: Option<Arc<LmtpServer>>, + lmtp_server: Option<Arc<LmtpServer>>, + imap_server: ImapServer<AddrIncoming, service::Instance>, } impl Server { - pub fn new(config: Config) -> Result<Self> { - let s3_region = Region::Custom { - name: config.aws_region.clone(), - endpoint: config.s3_endpoint, - }; - let k2v_region = Region::Custom { - name: config.aws_region, - endpoint: config.k2v_endpoint, - }; - let login_provider: Arc<dyn LoginProvider + Send + Sync> = - match (config.login_static, config.login_ldap) { - (Some(st), None) => Arc::new(StaticLoginProvider::new(st, k2v_region, s3_region)?), - (None, Some(ld)) => Arc::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?), - (Some(_), Some(_)) => { - bail!("A single login provider must be set up in config file") - } - (None, None) => bail!("No login provider is set up in config file"), - }; - - let lmtp_server = config - .lmtp - .map(|cfg| LmtpServer::new(cfg, login_provider.clone())); + pub async fn new(config: Config) -> Result<Self> { + let lmtp_config = config.lmtp.clone(); //@FIXME + let login = authenticator(config)?; + + let lmtp = lmtp_config.map(|cfg| LmtpServer::new(cfg, login.clone())); + + let incoming = AddrIncoming::new("127.0.0.1:4567").await?; + let imap = ImapServer::new(incoming).serve(service::Instance::new(login.clone())); Ok(Self { - login_provider, - lmtp_server, + lmtp_server: lmtp, + imap_server: imap, }) } - pub async fn run(&self) -> Result<()> { + + pub async fn run(self) -> Result<()> { + //tracing::info!("Starting server on {:#}", self.imap.incoming.local_addr); + tracing::info!("Starting Aerogramme..."); + let (exit_signal, provoke_exit) = watch_ctrl_c(); let exit_on_err = move |err: anyhow::Error| { error!("Error: {}", err); let _ = provoke_exit.send(true); }; + try_join!(async { match self.lmtp_server.as_ref() { None => Ok(()), Some(s) => s.run(exit_signal.clone()).await, } - })?; - Ok(()) - } - - pub async fn test(&self) -> Result<()> { - let creds = self.login_provider.login("lx", "plop").await?; + }, + //@FIXME handle ctrl + c + async { + self.imap_server.await?; + Ok(()) + } + )?; - let mut mailbox = Mailbox::new(&creds, "TestMailbox".to_string()).await?; - - mailbox.test().await?; Ok(()) } } +fn authenticator(config: Config) -> Result<Arc<dyn LoginProvider + Send + Sync>> { + let s3_region = Region::Custom { + name: config.aws_region.clone(), + endpoint: config.s3_endpoint, + }; + let k2v_region = Region::Custom { + name: config.aws_region, + endpoint: config.k2v_endpoint, + }; + + let lp: Arc<dyn LoginProvider + Send + Sync> = match (config.login_static, config.login_ldap) { + (Some(st), None) => Arc::new(StaticLoginProvider::new(st, k2v_region, s3_region)?), + (None, Some(ld)) => Arc::new(LdapLoginProvider::new(ld, k2v_region, s3_region)?), + (Some(_), Some(_)) => { + bail!("A single login provider must be set up in config file") + } + (None, None) => bail!("No login provider is set up in config file"), + }; + Ok(lp) +} + pub fn watch_ctrl_c() -> (watch::Receiver<bool>, Arc<watch::Sender<bool>>) { let (send_cancel, watch_cancel) = watch::channel(false); let send_cancel = Arc::new(send_cancel); diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..ce272a3 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,62 @@ +use std::sync::Arc; +use std::task::{Context, Poll}; + +use anyhow::Result; +use boitalettres::errors::Error as BalError; +use boitalettres::proto::{Request, Response}; +use boitalettres::server::accept::addr::AddrStream; +use futures::future::BoxFuture; +use futures::future::FutureExt; +use tower::Service; + +use crate::session; +use crate::LoginProvider; + +pub struct Instance { + login_provider: Arc<dyn LoginProvider + Send + Sync>, +} +impl Instance { + pub fn new(login_provider: Arc<dyn LoginProvider + Send + Sync>) -> Self { + Self { login_provider } + } +} +impl<'a> Service<&'a AddrStream> for Instance { + type Response = Connection; + type Error = anyhow::Error; + type Future = BoxFuture<'static, Result<Self::Response>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, addr: &'a AddrStream) -> Self::Future { + tracing::info!(remote_addr = %addr.remote_addr, local_addr = %addr.local_addr, "accept"); + let lp = self.login_provider.clone(); + async { Ok(Connection::new(lp)) }.boxed() + } +} + +pub struct Connection { + session: session::Manager, +} +impl Connection { + pub fn new(login_provider: Arc<dyn LoginProvider + Send + Sync>) -> Self { + Self { + session: session::Manager::new(login_provider), + } + } +} +impl Service<Request> for Connection { + type Response = Response; + type Error = BalError; + type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { + Poll::Ready(Ok(())) + } + + fn call(&mut self, req: Request) -> Self::Future { + tracing::debug!("Got request: {:#?}", req); + self.session.process(req) + } +} diff --git a/src/session.rs b/src/session.rs new file mode 100644 index 0000000..a3e4e24 --- /dev/null +++ b/src/session.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use boitalettres::errors::Error as BalError; +use boitalettres::proto::{Request, Response}; +use futures::future::BoxFuture; +use futures::future::FutureExt; +use imap_codec::types::command::CommandBody; +use tokio::sync::mpsc::error::TrySendError; +use tokio::sync::{mpsc, oneshot}; + +use crate::command; +use crate::login::Credentials; +use crate::mailbox::Mailbox; +use crate::LoginProvider; + +/* This constant configures backpressure in the system, + * or more specifically, how many pipelined messages are allowed + * before refusing them + */ +const MAX_PIPELINED_COMMANDS: usize = 10; + +struct Message { + req: Request, + tx: oneshot::Sender<Result<Response, BalError>>, +} + +pub struct Manager { + tx: mpsc::Sender<Message>, +} + +//@FIXME we should garbage collect the Instance when the Manager is destroyed. +impl Manager { + pub fn new(login_provider: Arc<dyn LoginProvider + Send + Sync>) -> Self { + let (tx, rx) = mpsc::channel(MAX_PIPELINED_COMMANDS); + tokio::spawn(async move { + let mut instance = Instance::new(login_provider, rx); + instance.start().await; + }); + Self { tx } + } + + pub fn process(&self, req: Request) -> BoxFuture<'static, Result<Response, BalError>> { + let (tx, rx) = oneshot::channel(); + let msg = Message { req, tx }; + + // We use try_send on a bounded channel to protect the daemons from DoS. + // Pipelining requests in IMAP are a special case: they should not occure often + // and in a limited number (like 3 requests). Someone filling the channel + // will probably be malicious so we "rate limit" them. + match self.tx.try_send(msg) { + Ok(()) => (), + Err(TrySendError::Full(_)) => { + return async { Response::bad("Too fast! Send less pipelined requests!") }.boxed() + } + Err(TrySendError::Closed(_)) => { + return async { Response::bad("The session task has exited") }.boxed() + } + }; + + // @FIXME add a timeout, handle a session that fails. + async { + match rx.await { + Ok(r) => r, + Err(e) => { + tracing::warn!("Got error {:#?}", e); + Response::bad("No response from the session handler") + } + } + } + .boxed() + } +} + +pub struct User { + pub name: String, + pub creds: Credentials, +} + +pub struct Instance { + rx: mpsc::Receiver<Message>, + + pub login_provider: Arc<dyn LoginProvider + Send + Sync>, + pub selected: Option<Mailbox>, + pub user: Option<User>, +} +impl Instance { + fn new(login_provider: Arc<dyn LoginProvider + Send + Sync>, rx: mpsc::Receiver<Message>) -> Self { + Self { + login_provider, + rx, + selected: None, + user: None, + } + } + + //@FIXME add a function that compute the runner's name from its local info + // to ease debug + // fn name(&self) -> String { } + + async fn start(&mut self) { + //@FIXME add more info about the runner + tracing::debug!("starting runner"); + + while let Some(msg) = self.rx.recv().await { + let mut cmd = command::Command::new(msg.req.tag, self); + let res = match msg.req.body { + CommandBody::Capability => cmd.capability().await, + CommandBody::Login { username, password } => cmd.login(username, password).await, + CommandBody::Lsub { + reference, + mailbox_wildcard, + } => cmd.lsub(reference, mailbox_wildcard).await, + CommandBody::List { + reference, + mailbox_wildcard, + } => cmd.list(reference, mailbox_wildcard).await, + CommandBody::Select { mailbox } => cmd.select(mailbox).await, + CommandBody::Fetch { + sequence_set, + attributes, + uid, + } => cmd.fetch(sequence_set, attributes, uid).await, + _ => Response::bad("Error in IMAP command received by server.") + .map_err(anyhow::Error::new), + }; + + let wrapped_res = res.or_else(|e| match e.downcast::<BalError>() { + Ok(be) => Err(be), + Err(ae) => { + tracing::warn!(error=%ae, "internal.error"); + Response::bad("Internal error") + } + }); + + //@FIXME I think we should quit this thread on error and having our manager watch it, + // and then abort the session as it is corrupted. + msg.tx.send(wrapped_res).unwrap_or_else(|e| { + tracing::warn!("failed to send imap response to manager: {:#?}", e) + }); + } + + //@FIXME add more info about the runner + tracing::debug!("exiting runner"); + } +} diff --git a/src/uidindex.rs b/src/uidindex.rs index ecd52ff..8e4a189 100644 --- a/src/uidindex.rs +++ b/src/uidindex.rs @@ -1,19 +1,28 @@ -use im::OrdMap; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use im::{HashMap, HashSet, OrdMap, OrdSet}; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use crate::bayou::*; -use crate::mail_uuid::MailUuid; +use crate::mail_ident::MailIdent; -type ImapUid = u32; -type ImapUidvalidity = u32; +pub type ImapUid = u32; +pub type ImapUidvalidity = u32; +pub type Flag = String; #[derive(Clone)] +/// A UidIndex handles the mutable part of a mailbox +/// It is built by running the event log on it +/// Each applied log generates a new UidIndex by cloning the previous one +/// and applying the event. This is why we use immutable datastructures: +/// they are cheap to clone. pub struct UidIndex { - pub mail_uid: OrdMap<MailUuid, ImapUid>, - pub mail_flags: OrdMap<MailUuid, Vec<String>>, + // Source of trust + pub table: OrdMap<MailIdent, (ImapUid, Vec<Flag>)>, - pub mails_by_uid: OrdMap<ImapUid, MailUuid>, + // Indexes optimized for queries + pub idx_by_uid: OrdMap<ImapUid, MailIdent>, + pub idx_by_flag: FlagIndex, + // Counters pub uidvalidity: ImapUidvalidity, pub uidnext: ImapUid, pub internalseq: ImapUid, @@ -21,40 +30,66 @@ pub struct UidIndex { #[derive(Clone, Serialize, Deserialize, Debug)] pub enum UidIndexOp { - MailAdd(MailUuid, ImapUid, Vec<String>), - MailDel(MailUuid), - FlagAdd(MailUuid, Vec<String>), - FlagDel(MailUuid, Vec<String>), + MailAdd(MailIdent, ImapUid, Vec<Flag>), + MailDel(MailIdent), + FlagAdd(MailIdent, Vec<Flag>), + FlagDel(MailIdent, Vec<Flag>), } impl UidIndex { #[must_use] - pub fn op_mail_add(&self, uuid: MailUuid, flags: Vec<String>) -> UidIndexOp { - UidIndexOp::MailAdd(uuid, self.internalseq, flags) + pub fn op_mail_add(&self, ident: MailIdent, flags: Vec<Flag>) -> UidIndexOp { + UidIndexOp::MailAdd(ident, self.internalseq, flags) } #[must_use] - pub fn op_mail_del(&self, uuid: MailUuid) -> UidIndexOp { - UidIndexOp::MailDel(uuid) + pub fn op_mail_del(&self, ident: MailIdent) -> UidIndexOp { + UidIndexOp::MailDel(ident) } #[must_use] - pub fn op_flag_add(&self, uuid: MailUuid, flags: Vec<String>) -> UidIndexOp { - UidIndexOp::FlagAdd(uuid, flags) + pub fn op_flag_add(&self, ident: MailIdent, flags: Vec<Flag>) -> UidIndexOp { + UidIndexOp::FlagAdd(ident, flags) } #[must_use] - pub fn op_flag_del(&self, uuid: MailUuid, flags: Vec<String>) -> UidIndexOp { - UidIndexOp::FlagDel(uuid, flags) + pub fn op_flag_del(&self, ident: MailIdent, flags: Vec<Flag>) -> UidIndexOp { + UidIndexOp::FlagDel(ident, flags) + } + + // INTERNAL functions to keep state consistent + + fn reg_email(&mut self, ident: MailIdent, uid: ImapUid, flags: &Vec<Flag>) { + // Insert the email in our table + self.table.insert(ident, (uid, flags.clone())); + + // Update the indexes/caches + self.idx_by_uid.insert(uid, ident); + self.idx_by_flag.insert(uid, flags); + } + + fn unreg_email(&mut self, ident: &MailIdent) { + // We do nothing if the mail does not exist + let (uid, flags) = match self.table.get(ident) { + Some(v) => v, + None => return, + }; + + // Delete all cache entries + self.idx_by_uid.remove(uid); + self.idx_by_flag.remove(*uid, flags); + + // Remove from source of trust + self.table.remove(ident); } } impl Default for UidIndex { fn default() -> Self { Self { - mail_flags: OrdMap::new(), - mail_uid: OrdMap::new(), - mails_by_uid: OrdMap::new(), + table: OrdMap::new(), + idx_by_uid: OrdMap::new(), + idx_by_flag: FlagIndex::new(), uidvalidity: 1, uidnext: 1, internalseq: 1, @@ -68,42 +103,53 @@ impl BayouState for UidIndex { fn apply(&self, op: &UidIndexOp) -> Self { let mut new = self.clone(); match op { - UidIndexOp::MailAdd(uuid, uid, flags) => { + UidIndexOp::MailAdd(ident, uid, flags) => { + // Change UIDValidity if there is a conflict if *uid < new.internalseq { new.uidvalidity += new.internalseq - *uid; } + + // Assign the real uid of the email let new_uid = new.internalseq; - if let Some(prev_uid) = new.mail_uid.get(uuid) { - new.mails_by_uid.remove(prev_uid); - } else { - new.mail_flags.insert(*uuid, flags.clone()); - } - new.mails_by_uid.insert(new_uid, *uuid); - new.mail_uid.insert(*uuid, new_uid); + // Delete the previous entry if any. + // Our proof has no assumption on `ident` uniqueness, + // so we must handle this case even it is very unlikely + // In this case, we overwrite the email. + // Note: assigning a new UID is mandatory. + new.unreg_email(ident); + + // We record our email and update ou caches + new.reg_email(*ident, new_uid, flags); + // Update counters new.internalseq += 1; new.uidnext = new.internalseq; } - UidIndexOp::MailDel(uuid) => { - if let Some(uid) = new.mail_uid.get(uuid) { - new.mails_by_uid.remove(uid); - new.mail_uid.remove(uuid); - new.mail_flags.remove(uuid); - } + UidIndexOp::MailDel(ident) => { + // If the email is known locally, we remove its references in all our indexes + new.unreg_email(ident); + + // We update the counter new.internalseq += 1; } - UidIndexOp::FlagAdd(uuid, new_flags) => { - let mail_flags = new.mail_flags.entry(*uuid).or_insert(vec![]); - for flag in new_flags { - if !mail_flags.contains(flag) { - mail_flags.push(flag.to_string()); - } + UidIndexOp::FlagAdd(ident, new_flags) => { + if let Some((uid, existing_flags)) = new.table.get_mut(ident) { + // Add flags to the source of trust and the cache + let mut to_add: Vec<Flag> = new_flags + .iter() + .filter(|f| !existing_flags.contains(f)) + .cloned() + .collect(); + new.idx_by_flag.insert(*uid, &to_add); + existing_flags.append(&mut to_add); } } - UidIndexOp::FlagDel(uuid, rm_flags) => { - if let Some(mail_flags) = new.mail_flags.get_mut(uuid) { - mail_flags.retain(|x| !rm_flags.contains(x)); + UidIndexOp::FlagDel(ident, rm_flags) => { + if let Some((uid, existing_flags)) = new.table.get_mut(ident) { + // Remove flags from the source of trust and the cache + existing_flags.retain(|x| !rm_flags.contains(x)); + new.idx_by_flag.remove(*uid, rm_flags); } } } @@ -111,11 +157,34 @@ impl BayouState for UidIndex { } } +// ---- FlagIndex implementation ---- +#[derive(Clone)] +pub struct FlagIndex(HashMap<Flag, OrdSet<ImapUid>>); + +impl FlagIndex { + fn new() -> Self { + Self(HashMap::new()) + } + fn insert(&mut self, uid: ImapUid, flags: &Vec<Flag>) { + flags.iter().for_each(|flag| { + self.0 + .entry(flag.clone()) + .or_insert(OrdSet::new()) + .insert(uid); + }); + } + fn remove(&mut self, uid: ImapUid, flags: &Vec<Flag>) -> () { + flags.iter().for_each(|flag| { + self.0.get_mut(flag).and_then(|set| set.remove(&uid)); + }); + } +} + // ---- CUSTOM SERIALIZATION AND DESERIALIZATION ---- #[derive(Serialize, Deserialize)] struct UidIndexSerializedRepr { - mails: Vec<(ImapUid, MailUuid, Vec<String>)>, + mails: Vec<(ImapUid, MailIdent, Vec<Flag>)>, uidvalidity: ImapUidvalidity, uidnext: ImapUid, internalseq: ImapUid, @@ -129,19 +198,17 @@ impl<'de> Deserialize<'de> for UidIndex { let val: UidIndexSerializedRepr = UidIndexSerializedRepr::deserialize(d)?; let mut uidindex = UidIndex { - mail_flags: OrdMap::new(), - mail_uid: OrdMap::new(), - mails_by_uid: OrdMap::new(), + table: OrdMap::new(), + idx_by_uid: OrdMap::new(), + idx_by_flag: FlagIndex::new(), uidvalidity: val.uidvalidity, uidnext: val.uidnext, internalseq: val.internalseq, }; - for (uid, uuid, flags) in val.mails { - uidindex.mail_flags.insert(uuid, flags); - uidindex.mail_uid.insert(uuid, uid); - uidindex.mails_by_uid.insert(uid, uuid); - } + val.mails + .iter() + .for_each(|(u, i, f)| uidindex.reg_email(*i, *u, f)); Ok(uidindex) } @@ -153,12 +220,8 @@ impl Serialize for UidIndex { S: Serializer, { let mut mails = vec![]; - for (uid, uuid) in self.mails_by_uid.iter() { - mails.push(( - *uid, - *uuid, - self.mail_flags.get(uuid).cloned().unwrap_or_default(), - )); + for (ident, (uid, flags)) in self.table.iter() { + mails.push((*uid, *ident, flags.clone())); } let val = UidIndexSerializedRepr { @@ -171,3 +234,99 @@ impl Serialize for UidIndex { val.serialize(serializer) } } + +// ---- TESTS ---- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_uidindex() { + let mut state = UidIndex::default(); + + // Add message 1 + { + let m = MailIdent([0x01; 24]); + let f = vec!["\\Recent".to_string(), "\\Archive".to_string()]; + let ev = state.op_mail_add(m, f); + state = state.apply(&ev); + + // Early checks + assert_eq!(state.table.len(), 1); + let (uid, flags) = state.table.get(&m).unwrap(); + assert_eq!(*uid, 1); + assert_eq!(flags.len(), 2); + let ident = state.idx_by_uid.get(&1).unwrap(); + assert_eq!(&m, ident); + let recent = state.idx_by_flag.0.get("\\Recent").unwrap(); + assert_eq!(recent.len(), 1); + assert_eq!(recent.iter().next().unwrap(), &1); + assert_eq!(state.uidnext, 2); + assert_eq!(state.uidvalidity, 1); + } + + // Add message 2 + { + let m = MailIdent([0x02; 24]); + let f = vec!["\\Seen".to_string(), "\\Archive".to_string()]; + let ev = state.op_mail_add(m, f); + state = state.apply(&ev); + + let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); + assert_eq!(archive.len(), 2); + } + + // Add flags to message 1 + { + let m = MailIdent([0x01; 24]); + let f = vec!["Important".to_string(), "$cl_1".to_string()]; + let ev = state.op_flag_add(m, f); + state = state.apply(&ev); + } + + // Delete flags from message 1 + { + let m = MailIdent([0x01; 24]); + let f = vec!["\\Recent".to_string()]; + let ev = state.op_flag_del(m, f); + state = state.apply(&ev); + + let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); + assert_eq!(archive.len(), 2); + } + + // Delete message 2 + { + let m = MailIdent([0x02; 24]); + let ev = state.op_mail_del(m); + state = state.apply(&ev); + + let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); + assert_eq!(archive.len(), 1); + } + + // Add a message 3 concurrent to message 1 (trigger a uid validity change) + { + let m = MailIdent([0x03; 24]); + let f = vec!["\\Archive".to_string(), "\\Recent".to_string()]; + let ev = UidIndexOp::MailAdd(m, 1, f); + state = state.apply(&ev); + } + + // Checks + { + assert_eq!(state.table.len(), 2); + assert!(state.uidvalidity > 1); + + let (last_uid, ident) = state.idx_by_uid.get_max().unwrap(); + assert_eq!(ident, &MailIdent([0x03; 24])); + + let archive = state.idx_by_flag.0.get("\\Archive").unwrap(); + assert_eq!(archive.len(), 2); + let mut iter = archive.iter(); + assert_eq!(iter.next().unwrap(), &1); + assert_eq!(iter.next().unwrap(), last_uid); + } + } +} |