aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock498
-rw-r--r--Cargo.toml8
-rw-r--r--README.md68
-rw-r--r--rust-toolchain.toml3
-rw-r--r--src/.server.rs.swobin0 -> 12288 bytes
-rw-r--r--src/.service.rs.swobin0 -> 12288 bytes
-rw-r--r--src/.session.rs.swobin0 -> 20480 bytes
-rw-r--r--src/command.rs112
-rw-r--r--src/config.rs12
-rw-r--r--src/lmtp.rs4
-rw-r--r--src/login/static_provider.rs6
-rw-r--r--src/mail_ident.rs (renamed from src/mail_uuid.rs)38
-rw-r--r--src/mailbox.rs52
-rw-r--r--src/mailstore.rs33
-rw-r--r--src/main.rs17
-rw-r--r--src/server.rs93
-rw-r--r--src/service.rs62
-rw-r--r--src/session.rs145
-rw-r--r--src/uidindex.rs281
19 files changed, 1224 insertions, 208 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 2e5416c..a467936 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index 00acdca..4393d1c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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" }
diff --git a/README.md b/README.md
index 48d1088..215fc17 100644
--- a/README.md
+++ b/README.md
@@ -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
new file mode 100644
index 0000000..9e99bb3
--- /dev/null
+++ b/src/.server.rs.swo
Binary files differ
diff --git a/src/.service.rs.swo b/src/.service.rs.swo
new file mode 100644
index 0000000..a69e975
--- /dev/null
+++ b/src/.service.rs.swo
Binary files differ
diff --git a/src/.session.rs.swo b/src/.session.rs.swo
new file mode 100644
index 0000000..a6de20e
--- /dev/null
+++ b/src/.session.rs.swo
Binary files differ
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);
+ }
+ }
+}