Compare commits

...

35 Commits
0.2.0 ... main

Author SHA1 Message Date
Quentin 0dcf69f180
bump rust toolchain + fix publish script bug 2024-02-24 12:24:51 +01:00
Quentin d92ae5220c Merge pull request 'Perf measurement & bottleneck fix' (#102) from perf/cpu-ram-bottleneck into main
Reviewed-on: #102
2024-02-23 17:32:38 +00:00
Quentin 1ea3de3099
bumping to 0.2.2 2024-02-23 18:32:09 +01:00
Quentin 0b122582e8
fix code formatting 2024-02-23 18:28:04 +01:00
Quentin ab03c7a160
Upgrade Cargo.nix 2024-02-23 18:27:38 +01:00
Quentin 2a084df300
Also share HTTPClient for K2V 2024-02-23 17:31:29 +01:00
Quentin 02a8537556
Replace with a single AWS HTTP client 2024-02-23 17:01:51 +01:00
Quentin a579382042
update flake dependency 2024-02-23 08:46:05 +01:00
Quentin 38a8c7de2a
upgrade cargo2nix 2024-02-22 17:32:18 +01:00
Quentin 9b26e251e3
formatting 2024-02-22 17:31:03 +01:00
Quentin 2adf73dd8e
Update imap-flow, clean IDLE 2024-02-22 17:30:40 +01:00
Quentin 3f204b102a
fix test 2024-02-22 11:51:58 +01:00
Quentin 4d501b6947
Compile streams 2024-02-22 11:35:39 +01:00
Quentin de5717a020
Upgrade Cargo.nix 2024-02-20 16:02:56 +01:00
Quentin 64b474f682
Unsollicited response on APPEND was wrong, upgrade imap-flow to fix LITERAL+ 2024-02-20 13:24:42 +01:00
Quentin 28b1f4f14d
Unsollicited responses on APPEND 2024-02-20 11:42:51 +01:00
Quentin 4aa31ba8b5
Add datasets 2024-02-16 18:55:46 +01:00
Quentin 0b20d726bb
Add a 100 emails dataset on Git LFS 2024-02-15 11:12:20 +01:00
Quentin 0bb7cdf696
Set pipelinable commands to 64 2024-02-15 11:04:10 +01:00
Quentin d50b1dc178 Merge pull request 'Debug the Dovecot Auth Protocol' (#95) from bug/dovecot-auth-resp into main
Reviewed-on: #95
2024-02-13 16:13:53 +00:00
Quentin 9377ca3ef4
Accept authz id == auth id 2024-02-13 16:57:01 +01:00
Quentin 25e716a17f
dovecot plain auth inline continuation support 2024-02-13 11:21:11 +01:00
Quentin e778bebfd3
Fix nix develop 2024-02-13 10:32:11 +01:00
Quentin ede836fc80
automate publishing with nix 2024-02-10 18:04:27 +01:00
Quentin 3dfe914fda
add building scripts 2024-02-10 17:29:32 +01:00
Quentin 9954cea30f
fix cargo.nix 2024-02-10 13:44:02 +01:00
Quentin 3b675ac357 Merge pull request 'WIP 0.2.1' (#93) from bug/deployment into main
Reviewed-on: #93
2024-02-10 11:11:55 +00:00
Quentin 0e3cfe536f
Escape LMTP data 2024-02-10 12:11:01 +01:00
Quentin 599480c3d3
Switch version to 0.2.1 2024-02-08 19:41:40 +01:00
Quentin 59f4bdf9d0
fix idle loop error 2024-02-08 19:40:43 +01:00
Quentin 678c5bacc6
add way more logging 2024-02-08 15:12:52 +01:00
Quentin 22f0eb901a
format + fix storage bug 2024-01-31 11:01:18 +01:00
Quentin c27919a757
upgrade k2v to 0.9.1 2024-01-30 17:34:16 +01:00
Quentin 1d6344363a
retrieve missing attributes ldap 2024-01-30 15:45:48 +01:00
Quentin 93c0aa4b3a
Various post-release fixes 2024-01-25 11:35:33 +01:00
34 changed files with 4334 additions and 1034 deletions

481
Cargo.lock generated
View File

@ -28,13 +28,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aerogramme"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"anyhow",
"argon2",
"async-trait",
"aws-config",
"aws-sdk-s3",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"backtrace",
"base64 0.21.7",
"chrono",
@ -44,7 +46,8 @@ dependencies = [
"eml-codec",
"futures",
"hex",
"hyper-rustls",
"hyper-rustls 0.26.0",
"hyper-util",
"im",
"imap-codec",
"imap-flow",
@ -455,28 +458,27 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "1.1.2"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e64b72d4bdbb41a73d27709c65a25b6e4bfc8321bf70fa3a8b19ce7d4eb81b0"
checksum = "3182c19847238b50b62ae0383a6dbfc14514e552eb5e307e1ea83ccf5840b8a6"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-sdk-sso",
"aws-sdk-ssooidc",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand 2.0.1",
"hex",
"http",
"hyper",
"http 0.2.11",
"hyper 0.14.28",
"ring 0.17.7",
"time",
"tokio",
@ -486,77 +488,84 @@ dependencies = [
[[package]]
name = "aws-credential-types"
version = "1.1.2"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a7cb3510b95492bd9014b60e2e3bee3e48bc516e220316f8e6b60df18b47331"
checksum = "e5635d8707f265c773282a22abe1ecd4fbe96a8eb2f0f14c0796f8016f11a41a"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"zeroize",
]
[[package]]
name = "aws-http"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a95d41abe4e941399fdb4bc2f54713eac3c839d98151875948bb24e66ab658f2"
dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-types",
"bytes",
"http",
"http-body",
"pin-project-lite 0.2.13",
"tracing",
]
[[package]]
name = "aws-runtime"
version = "1.1.2"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233cca219c6705d525ace011d6f9bc51aaf32fce5b4c41661d2d7ff22d9b4d49"
checksum = "6f82b9ae2adfd9d6582440d0eeb394c07f74d21b4c0cc72bdb73735c9e1a9c0e"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-sigv4 1.1.2",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-eventstream",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand 2.0.1",
"http",
"http 0.2.11",
"http-body 0.4.6",
"percent-encoding",
"pin-project-lite 0.2.13",
"tracing",
"uuid",
]
[[package]]
name = "aws-sdk-s3"
version = "1.12.0"
name = "aws-sdk-config"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "634fbe5b6591ee2e281cd2ba8641e9bd752dbf5bf338924d6ad4bd5a3304fe31"
checksum = "0cb71960e3e197c3f512f3bf0f47f444acd708db59733416107ec2ff161ff5c4"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-sigv4 1.1.2",
"aws-smithy-async",
"aws-smithy-checksums",
"aws-smithy-eventstream",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-types",
"bytes",
"http 0.2.11",
"once_cell",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-s3"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5076637347e7d0218e61facae853110682ae58efabd2f4e2a9e530c203d5fa7b"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-checksums",
"aws-smithy-eventstream",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"http",
"http-body",
"http 0.2.11",
"http-body 0.4.6",
"once_cell",
"percent-encoding",
"regex-lite",
@ -566,22 +575,21 @@ dependencies = [
[[package]]
name = "aws-sdk-sso"
version = "1.10.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee41005e0f3a19ae749c7953d9e1f1ef8d2183f76f64966e346fa41c1ba0ed44"
checksum = "ca7e8097448832fcd22faf6bb227e97d76b40e354509d1307653a885811c7151"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http 0.2.11",
"once_cell",
"regex-lite",
"tracing",
@ -589,22 +597,21 @@ dependencies = [
[[package]]
name = "aws-sdk-ssooidc"
version = "1.10.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa08168f8a27505e7b90f922c32a489feb1f2133878981a15138bebc849ac09c"
checksum = "a75073590e23d63044606771afae309fada8eb10ded54a1ce4598347221d3fef"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http 0.2.11",
"once_cell",
"regex-lite",
"tracing",
@ -612,23 +619,22 @@ dependencies = [
[[package]]
name = "aws-sdk-sts"
version = "1.10.0"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29102eff04d50ef70f11a48823db33e33c6cc5f027bfb6ff4864efbd5f1f66f3"
checksum = "650e4aaae41547151dea4d8142f7ffcc8ab8ba76d5dccc8933936ef2102c3356"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-query",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"http",
"http 0.2.11",
"once_cell",
"regex-lite",
"tracing",
@ -636,40 +642,22 @@ dependencies = [
[[package]]
name = "aws-sigv4"
version = "0.55.3"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c"
dependencies = [
"aws-smithy-http 0.55.3",
"form_urlencoded",
"hex",
"hmac",
"http",
"once_cell",
"percent-encoding",
"regex",
"sha2",
"time",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b92384b39aedb258aa734fe0e7b2ffcd13f33e68227251a72cd2635e0acc8f1a"
checksum = "404c64a104188ac70dd1684718765cb5559795458e446480e41984e68e57d888"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"bytes",
"crypto-bigint 0.5.5",
"form_urlencoded",
"hex",
"hmac",
"http",
"http 0.2.11",
"http 1.0.0",
"once_cell",
"p256",
"percent-encoding",
@ -683,9 +671,9 @@ dependencies = [
[[package]]
name = "aws-smithy-async"
version = "1.1.3"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eac0bb78e9e2765699999a02d7bfb4e6ad8f13e0962ebb9f5202b1d8cd76006"
checksum = "fcf7f09a27286d84315dfb9346208abb3b0973a692454ae6d0bc8d803fcce3b4"
dependencies = [
"futures-util",
"pin-project-lite 0.2.13",
@ -694,18 +682,18 @@ dependencies = [
[[package]]
name = "aws-smithy-checksums"
version = "0.60.3"
version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "535a2d5f1e459bc7709580a77152c8d493982db083236c2b1d1c51dc6217e8a3"
checksum = "0fd4b66f2a8e7c84d7e97bda2666273d41d2a2e25302605bcf906b7b2661ae5e"
dependencies = [
"aws-smithy-http 0.60.3",
"aws-smithy-types 1.1.3",
"aws-smithy-http",
"aws-smithy-types",
"bytes",
"crc32c",
"crc32fast",
"hex",
"http",
"http-body",
"http 0.2.11",
"http-body 0.4.6",
"md-5",
"pin-project-lite 0.2.13",
"sha1",
@ -715,49 +703,29 @@ dependencies = [
[[package]]
name = "aws-smithy-eventstream"
version = "0.60.3"
version = "0.60.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "682371561562d08ab437766903c6bc28f4f95d7ab2ecfb389bda7849dd98aefe"
checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858"
dependencies = [
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"bytes",
"crc32fast",
]
[[package]]
name = "aws-smithy-http"
version = "0.55.3"
version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28"
dependencies = [
"aws-smithy-types 0.55.3",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"hyper",
"once_cell",
"percent-encoding",
"pin-project-lite 0.2.13",
"pin-utils",
"tracing",
]
[[package]]
name = "aws-smithy-http"
version = "0.60.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365ca49744b2bda2f1e2dc03b856da3fa5a28ca5b0a41e41d7ff5305a8fae190"
checksum = "b6ca214a6a26f1b7ebd63aa8d4f5e2194095643023f9608edf99a58247b9d80d"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"http 0.2.11",
"http-body 0.4.6",
"once_cell",
"percent-encoding",
"pin-project-lite 0.2.13",
@ -767,40 +735,40 @@ dependencies = [
[[package]]
name = "aws-smithy-json"
version = "0.60.3"
version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "733ccdb727ac63370836aa3b3c483d75ad2ef7bc6507db3efe1d01e8d2e50367"
checksum = "1af80ecf3057fb25fe38d1687e94c4601a7817c6a1e87c1b0635f7ecb644ace5"
dependencies = [
"aws-smithy-types 1.1.3",
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.60.3"
version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aff02ae2ee7968bbce2983ffb5ce529d24f4848532300f398347bde8c2196974"
checksum = "eb27084f72ea5fc20033efe180618677ff4a2f474b53d84695cfe310a6526cbc"
dependencies = [
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-runtime"
version = "1.1.3"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab9cb6fee50680af8ceaa293ae79eba32095ca117161cb323f9ee30dd87d139"
checksum = "fbb5fca54a532a36ff927fbd7407a7c8eb9c3b4faf72792ba2965ea2cad8ed55"
dependencies = [
"aws-smithy-async",
"aws-smithy-http 0.60.3",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"bytes",
"fastrand 2.0.1",
"h2",
"http",
"http-body",
"hyper",
"hyper-rustls",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper-rustls 0.24.2",
"once_cell",
"pin-project-lite 0.2.13",
"pin-utils",
@ -811,14 +779,15 @@ dependencies = [
[[package]]
name = "aws-smithy-runtime-api"
version = "1.1.3"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02ca2da7619517310bfead6d18abcdde90f1439224d887d608503cfacff46dff"
checksum = "22389cb6f7cac64f266fb9f137745a9349ced7b47e0d2ba503e9e40ede4f7060"
dependencies = [
"aws-smithy-async",
"aws-smithy-types 1.1.3",
"aws-smithy-types",
"bytes",
"http",
"http 0.2.11",
"http 1.0.0",
"pin-project-lite 0.2.13",
"tokio",
"tracing",
@ -827,29 +796,16 @@ dependencies = [
[[package]]
name = "aws-smithy-types"
version = "0.55.3"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16a3d0bf4f324f4ef9793b86a1701d9700fbcdbd12a846da45eed104c634c6e8"
dependencies = [
"base64-simd",
"itoa",
"num-integer",
"ryu",
"time",
]
[[package]]
name = "aws-smithy-types"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4bb944488536cd2fef43212d829bc7e9a8bfc4afa079d21170441e7be8d2d0"
checksum = "f081da5481210523d44ffd83d9f0740320050054006c719eae0232d411f024d3"
dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"http 0.2.11",
"http-body 0.4.6",
"itoa",
"num-integer",
"pin-project-lite 0.2.13",
@ -863,24 +819,24 @@ dependencies = [
[[package]]
name = "aws-smithy-xml"
version = "0.60.3"
version = "0.60.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef796feaf894d7fd03869235237aeffe73ed1b29a3927cceeee2eecadf876eba"
checksum = "0fccd8f595d0ca839f9f2548e66b99514a85f92feb4c01cf2868d93eb4888a42"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "1.1.2"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8549aa62c5b7db5c57ab915200ee214b4f5d8f19b29a4a8fa0b3ad3bca1380e3"
checksum = "8fbb5d48aae496f628e7aa2e41991dd4074f606d9e3ade1ce1059f293d40f9a2"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types 1.1.3",
"http",
"aws-smithy-types",
"http 0.2.11",
"rustc_version",
"tracing",
]
@ -896,9 +852,9 @@ dependencies = [
"bitflags 1.3.2",
"bytes",
"futures-util",
"http",
"http-body",
"hyper",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
"itoa",
"matchit",
"memchr",
@ -922,8 +878,8 @@ dependencies = [
"async-trait",
"bytes",
"futures-util",
"http",
"http-body",
"http 0.2.11",
"http-body 0.4.6",
"mime",
"rustversion",
"tower-layer",
@ -1242,9 +1198,9 @@ dependencies = [
[[package]]
name = "crc32c"
version = "0.6.4"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74"
checksum = "89254598aa9b9fa608de44b3ae54c810f0f06d755e24c50177f1f8f31ff50ce2"
dependencies = [
"rustc_version",
]
@ -1437,7 +1393,8 @@ dependencies = [
[[package]]
name = "eml-codec"
version = "0.1.2"
source = "git+https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git?branch=main#a7bd3c475a58e42b86c163ec075ce01ddae7e60a"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4499124d87abce26a57ef96ece800fa8babc38fbedd81c607c340ae83d46d2e"
dependencies = [
"base64 0.21.7",
"chrono",
@ -1742,7 +1699,26 @@ dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"http",
"http 0.2.11",
"indexmap 2.1.0",
"slab",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
name = "h2"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943"
dependencies = [
"bytes",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http 1.0.0",
"indexmap 2.1.0",
"slab",
"tokio",
@ -1822,6 +1798,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
@ -1829,7 +1816,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http",
"http 0.2.11",
"pin-project-lite 0.2.13",
]
[[package]]
name = "http-body"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643"
dependencies = [
"bytes",
"http 1.0.0",
]
[[package]]
name = "http-body-util"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41cb79eb393015dadd30fc252023adb0b2400a0caee0fa2a077e6e21a551e840"
dependencies = [
"bytes",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"pin-project-lite 0.2.13",
]
@ -1861,9 +1871,9 @@ dependencies = [
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
@ -1875,6 +1885,27 @@ dependencies = [
"want",
]
[[package]]
name = "hyper"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"h2 0.4.2",
"http 1.0.0",
"http-body 1.0.0",
"httparse",
"httpdate",
"itoa",
"pin-project-lite 0.2.13",
"smallvec",
"tokio",
"want",
]
[[package]]
name = "hyper-rustls"
version = "0.24.2"
@ -1882,27 +1913,66 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590"
dependencies = [
"futures-util",
"http",
"hyper",
"http 0.2.11",
"hyper 0.14.28",
"log",
"rustls 0.21.10",
"rustls-native-certs",
"rustls-native-certs 0.6.3",
"tokio",
"tokio-rustls 0.24.1",
]
[[package]]
name = "hyper-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http 1.0.0",
"hyper 1.2.0",
"hyper-util",
"log",
"rustls 0.22.2",
"rustls-native-certs 0.7.0",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.25.0",
"tower-service",
]
[[package]]
name = "hyper-timeout"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
dependencies = [
"hyper",
"hyper 0.14.28",
"pin-project-lite 0.2.13",
"tokio",
"tokio-io-timeout",
]
[[package]]
name = "hyper-util"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa"
dependencies = [
"bytes",
"futures-channel",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"hyper 1.2.0",
"pin-project-lite 0.2.13",
"socket2 0.5.5",
"tokio",
"tower",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.59"
@ -1964,7 +2034,7 @@ dependencies = [
[[package]]
name = "imap-codec"
version = "2.0.0"
source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0adcc244282c64cc7874ffa9cd22e4a451ee19f8"
source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
dependencies = [
"abnf-core",
"base64 0.21.7",
@ -1979,19 +2049,21 @@ dependencies = [
[[package]]
name = "imap-flow"
version = "0.1.0"
source = "git+https://github.com/superboum/imap-flow.git?branch=custom/aerogramme#60ff9e082ccfcd10a042b616d8038a578fa0c8ff"
source = "git+https://github.com/duesee/imap-flow.git?branch=main#68c1da5d1c56dbe543d9736de9683259d1d28191"
dependencies = [
"bounded-static",
"bytes",
"imap-codec",
"imap-types",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "imap-types"
version = "2.0.0"
source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#0adcc244282c64cc7874ffa9cd22e4a451ee19f8"
source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed"
dependencies = [
"base64 0.21.7",
"bounded-static",
@ -2075,14 +2147,17 @@ dependencies = [
[[package]]
name = "k2v-client"
version = "0.0.4"
source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?tag=v0.9.0#952c9570c494468643353ee1ae9052b510353665"
source = "git+https://git.deuxfleurs.fr/Deuxfleurs/garage.git?branch=k2v/shared_http_client#8b35a946d9f6b31b26b9783acbfab984316051f4"
dependencies = [
"aws-sigv4 0.55.3",
"aws-sdk-config",
"aws-sigv4",
"base64 0.21.7",
"hex",
"http",
"hyper",
"hyper-rustls",
"http 1.0.0",
"http-body-util",
"hyper 1.2.0",
"hyper-rustls 0.26.0",
"hyper-util",
"log",
"percent-encoding",
"serde",
@ -2135,7 +2210,7 @@ dependencies = [
"percent-encoding",
"ring 0.16.20",
"rustls 0.20.9",
"rustls-native-certs",
"rustls-native-certs 0.6.3",
"thiserror",
"tokio",
"tokio-rustls 0.23.4",
@ -2904,6 +2979,19 @@ dependencies = [
"security-framework",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792"
dependencies = [
"openssl-probe",
"rustls-pemfile 2.0.0",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@ -3519,10 +3607,10 @@ dependencies = [
"axum",
"base64 0.21.7",
"bytes",
"h2",
"http",
"http-body",
"hyper",
"h2 0.3.24",
"http 0.2.11",
"http-body 0.4.6",
"hyper 0.14.28",
"hyper-timeout",
"percent-encoding",
"pin-project",
@ -3573,6 +3661,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite 0.2.13",
"tracing-attributes",
"tracing-core",

697
Cargo.nix vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "aerogramme"
version = "0.2.0"
version = "0.2.2"
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
edition = "2021"
license = "EUPL-1.2"
@ -48,23 +48,26 @@ rand = "0.8.5"
rustls = "0.22"
rustls-pemfile = "2.0"
tokio-rustls = "0.25"
hyper-rustls = { version = "0.24", features = ["http2"] }
hyper-rustls = { version = "0.26", features = ["http2"] }
hyper-util = { version = "0.1", features = ["full"] }
rpassword = "7.0"
# login
ldap3 = { version = "0.10", default-features = false, features = ["tls-rustls"] }
# storage
k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", tag = "v0.9.0" }
aws-config = { version = "1.1.1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1.9.0"
k2v-client = { git = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git", branch = "k2v/shared_http_client" }
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-s3 = "1"
aws-smithy-runtime = "1"
aws-smithy-runtime-api = "1"
# email protocols
eml-codec = { git = "https://git.deuxfleurs.fr/Deuxfleurs/eml-codec.git", branch = "main" }
eml-codec = "0.1.2"
smtp-message = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
smtp-server = { git = "http://github.com/Alexis211/kannader", branch = "feature/lmtp" }
imap-codec = { version = "2.0.0", features = ["bounded-static", "ext_condstore_qresync"] }
imap-flow = { git = "https://github.com/superboum/imap-flow.git", branch = "custom/aerogramme" }
imap-flow = { git = "https://github.com/duesee/imap-flow.git", branch = "main" }
thiserror = "1.0.56"
[dev-dependencies]

118
flake.lock vendored
View File

@ -1,10 +1,30 @@
{
"nodes": {
"albatros": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1684830446,
"narHash": "sha256-jyYwYYNKSe40Y9OirIkeFTvTvqNj0NErh4TNBJmujw4=",
"ref": "main",
"rev": "fb80c5d6734044ca7718989a3b36503b9463f1b2",
"revCount": 81,
"type": "git",
"url": "https://git.deuxfleurs.fr/Deuxfleurs/albatros.git"
},
"original": {
"ref": "main",
"type": "git",
"url": "https://git.deuxfleurs.fr/Deuxfleurs/albatros.git"
}
},
"cargo2nix": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
},
"locked": {
@ -24,20 +44,19 @@
},
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs_2",
"nixpkgs": "nixpkgs_3",
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1688484237,
"narHash": "sha256-qFUn2taHGe203wm7Oio4UGFz1sAiq+kitRexY3sQ1CA=",
"lastModified": 1708669354,
"narHash": "sha256-eGhZLjF59aF9bYdSOleT1BD94qvo1NgMio4vMKBzxgY=",
"owner": "nix-community",
"repo": "fenix",
"rev": "626a9e0a84010728b335f14d3982e11b99af7dc6",
"rev": "a0f0f781683e4e93b61beaf1dfee4dd34cf3a092",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "monthly",
"repo": "fenix",
"type": "github"
}
@ -59,6 +78,24 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"locked": {
"lastModified": 1667395993,
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
@ -73,16 +110,16 @@
"type": "github"
}
},
"flake-utils_2": {
"flake-utils_3": {
"inputs": {
"systems": "systems"
"systems": "systems_2"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"type": "github"
},
"original": {
@ -92,6 +129,21 @@
}
},
"nixpkgs": {
"locked": {
"lastModified": 1678964307,
"narHash": "sha256-POV15raLJzwns6U84W4aWNSeSJRXTz7xWQW6IcrWQns=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd4f7832961053e6095af8de8d6a57b5ad402f19",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1672580127,
"narHash": "sha256-3lW3xZslREhJogoOkjeZtlBtvFMyxHku7I/9IVehhT8=",
@ -107,13 +159,13 @@
"type": "github"
}
},
"nixpkgs_2": {
"nixpkgs_3": {
"locked": {
"lastModified": 1688231357,
"narHash": "sha256-ZOn16X5jZ6X5ror58gOJAxPfFLAQhZJ6nOUeS4tfFwo=",
"lastModified": 1706550542,
"narHash": "sha256-UcsnCG6wx++23yeER4Hg18CXWbgNpqNXcHIo5/1Y+hc=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "645ff62e09d294a30de823cb568e9c6d68e92606",
"rev": "97b17f32362e475016f942bbdfda4a4a72a8a652",
"type": "github"
},
"original": {
@ -123,13 +175,13 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs_4": {
"locked": {
"lastModified": 1690294827,
"narHash": "sha256-JV53dEaMM566e+6R4Wj58jBAkFg7HaZr3SsXZ9hdh40=",
"lastModified": 1708673722,
"narHash": "sha256-FPbPhA727wuVkmR21Va6scRjAmj4pk3U8blteaXB/Hg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7ce0abe77d2ace6d6fc43ff7077019e62a77e741",
"rev": "92cf4feb2b9091466a82b27e4bb045cbccc2ba09",
"type": "github"
},
"original": {
@ -141,20 +193,21 @@
},
"root": {
"inputs": {
"albatros": "albatros",
"cargo2nix": "cargo2nix",
"fenix": "fenix",
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
"flake-utils": "flake-utils_3",
"nixpkgs": "nixpkgs_4"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1688410727,
"narHash": "sha256-TqKZO9D64UDBCMY2sUP2ebAKP0oY7S9enrHfZaDiqBQ=",
"lastModified": 1706735270,
"narHash": "sha256-IJk+UitcJsxzMQWm9pa1ZbJBriQ4ginXOlPyVq+Cu40=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "45272efec5fcb8bc46e303d6ced8bd2ba095a667",
"rev": "42cb1a2bd79af321b0cc503d2960b73f34e2f92b",
"type": "github"
},
"original": {
@ -203,6 +256,21 @@
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

127
flake.nix
View File

@ -14,12 +14,14 @@
};
# use rust project builds
fenix.url = "github:nix-community/fenix/monthly";
fenix.url = "github:nix-community/fenix";
# import alba releasing tool
albatros.url = "git+https://git.deuxfleurs.fr/Deuxfleurs/albatros.git?ref=main";
};
outputs = { self, nixpkgs, cargo2nix, flake-utils, fenix }:
flake-utils.lib.eachSystem [
"x86_64-linux"
outputs = { self, nixpkgs, cargo2nix, flake-utils, fenix, albatros }:
let platformArtifacts = flake-utils.lib.eachSystem [
"x86_64-unknown-linux-musl"
"aarch64-unknown-linux-musl"
"armv6l-unknown-linux-musleabihf"
@ -52,21 +54,6 @@
];
};
pkgVanilla = import nixpkgs { system = "x86_64-linux"; };
shell = pkgVanilla.mkShell {
buildInputs = [
cargo2nix.packages.x86_64-linux.default
fenix.packages.x86_64-linux.minimal.toolchain
fenix.packages.x86_64-linux.rust-analyzer
];
shellHook = ''
echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.minimal.rustc}"
export RUST_SRC_PATH="${fenix.packages.x86_64-linux.latest.rust-src}/lib/rustlib/src/rust/library"
export RUST_ANALYZER_INTERNALS_DO_NOT_USE='this is unstable'
'';
};
rustTarget = if targetHost == "armv6l-unknown-linux-musleabihf" then "arm-unknown-linux-musleabihf" else targetHost;
# release builds
@ -74,8 +61,8 @@
packageFun = import ./Cargo.nix;
target = rustTarget;
release = true;
rustcLinkFlags = [ "--cfg" "tokio_unstable" ];
rustcBuildFlags = [ "--cfg" "tokio_unstable" ];
#rustcLinkFlags = [ "--cfg" "tokio_unstable" ];
#rustcBuildFlags = [ "--cfg" "tokio_unstable" ];
rustToolchain = with fenix.packages.x86_64-linux; combine [
minimal.cargo
minimal.rustc
@ -125,14 +112,29 @@
];
});
crate = (rustRelease.workspace.aerogramme {});
# binary extract
bin = pkgs.stdenv.mkDerivation {
pname = "aerogramme-bin";
version = "0.1.0";
pname = "${crate.name}-bin";
version = crate.version;
dontUnpack = true;
dontBuild = true;
installPhase = ''
cp ${(rustRelease.workspace.aerogramme {}).bin}/bin/aerogramme $out
cp ${crate.bin}/bin/aerogramme $out
'';
};
# fhs extract
fhs = pkgs.stdenv.mkDerivation {
pname = "${crate.name}-fhs";
version = crate.version;
dontUnpack = true;
dontBuild = true;
installPhase = ''
mkdir -p $out/bin
cp ${crate.bin}/bin/aerogramme $out/bin/
'';
};
@ -151,16 +153,81 @@
container = pkgs.dockerTools.buildImage {
name = "dxflrs/aerogramme";
architecture = (builtins.getAttr targetHost archMap).GOARCH;
copyToRoot = fhs;
config = {
Cmd = [ "${bin}" "server" ];
Env = [ "PATH=/bin" ];
Cmd = [ "aerogramme" "--dev" "provider" "daemon" ];
};
};
in {
devShells.default = shell;
packages.debug = (rustDebug.workspace.aerogramme {}).bin;
packages.aerogramme = bin;
packages.container = container;
packages.default = self.packages.${targetHost}.aerogramme;
meta = {
version = crate.version;
};
packages = {
inherit fhs container;
debug = (rustDebug.workspace.aerogramme {}).bin;
aerogramme = bin;
default = self.packages.${targetHost}.aerogramme;
};
});
###
#
# RELEASE STUFF
#
###
gpkgs = import nixpkgs {
system = "x86_64-linux"; # hardcoded as we will cross compile
};
alba = albatros.packages.x86_64-linux.alba;
# Shell
shell = gpkgs.mkShell {
buildInputs = [
cargo2nix.packages.x86_64-linux.default
fenix.packages.x86_64-linux.minimal.toolchain
fenix.packages.x86_64-linux.rust-analyzer
];
shellHook = ''
echo "AEROGRAME DEVELOPMENT SHELL ${fenix.packages.x86_64-linux.minimal.rustc}"
export RUST_SRC_PATH="${fenix.packages.x86_64-linux.latest.rust-src}/lib/rustlib/src/rust/library"
export RUST_ANALYZER_INTERNALS_DO_NOT_USE='this is unstable'
'';
};
# Used only to fetch the "version"
version = platformArtifacts.meta.x86_64-unknown-linux-musl.version;
build = gpkgs.writeScriptBin "aerogramme-build" ''
set -euxo pipefail
# static
nix build --print-build-logs .#packages.x86_64-unknown-linux-musl.aerogramme -o static/linux/amd64/aerogramme
nix build --print-build-logs .#packages.aarch64-unknown-linux-musl.aerogramme -o static/linux/arm64/aerogramme
nix build --print-build-logs .#packages.armv6l-unknown-linux-musleabihf.aerogramme -o static/linux/arm/aerogramme
# containers
nix build --print-build-logs .#packages.x86_64-unknown-linux-musl.container -o docker/linux.amd64.tar.gz
nix build --print-build-logs .#packages.aarch64-unknown-linux-musl.container -o docker/linux.arm64.tar.gz
nix build --print-build-logs .#packages.armv6l-unknown-linux-musleabihf.container -o docker/linux.arm.tar.gz
'';
push = gpkgs.writeScriptBin "aerogramme-publish" ''
set -euxo pipefail
${alba} static push -t aerogramme:${version} static/ 's3://download.deuxfleurs.org?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true&region=garage' 1>&2
${alba} container push -t aerogramme:${version} docker/ 's3://registry.deuxfleurs.org?endpoint=garage.deuxfleurs.fr&s3ForcePathStyle=true&region=garage' 1>&2
${alba} container push -t aerogramme:${version} docker/ "docker://docker.io/dxflrs/aerogramme:${version}" 1>&2
'';
in
{
devShells.x86_64-linux.default = shell;
packages = {
x86_64-linux = {
inherit build push;
};
} // platformArtifacts.packages;
};
}

View File

@ -1,6 +1,6 @@
use std::net::SocketAddr;
use anyhow::{Result, anyhow, bail};
use anyhow::{anyhow, bail, Result};
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::io::BufStream;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
@ -25,9 +25,9 @@ use crate::login::ArcLoginProvider;
/// C: VERSION 1 2
/// C: CPID 1
///
/// C: AUTH 2 PLAIN service=smtp
/// S: CONT 2
/// C: CONT 2 base64stringFollowingRFC4616==
/// C: AUTH 2 PLAIN service=smtp
/// S: CONT 2
/// C: CONT 2 base64stringFollowingRFC4616==
/// S: OK 2 user=alice@example.tld
///
/// C: AUTH 42 LOGIN service=smtp
@ -41,7 +41,7 @@ use crate::login::ArcLoginProvider;
/// ## RFC References
///
/// PLAIN SASL - https://datatracker.ietf.org/doc/html/rfc4616
///
///
///
/// ## Dovecot References
///
@ -54,22 +54,20 @@ pub struct AuthServer {
bind_addr: SocketAddr,
}
impl AuthServer {
pub fn new(
config: AuthConfig,
login_provider: ArcLoginProvider,
) -> Self {
pub fn new(config: AuthConfig, login_provider: ArcLoginProvider) -> Self {
Self {
bind_addr: config.bind_addr,
login_provider,
}
}
pub async fn run(self: Self, mut must_exit: watch::Receiver<bool>) -> Result<()> {
let tcp = TcpListener::bind(self.bind_addr).await?;
tracing::info!("SASL Authentication Protocol listening on {:#}", self.bind_addr);
tracing::info!(
"SASL Authentication Protocol listening on {:#}",
self.bind_addr
);
let mut connections = FuturesUnordered::new();
@ -89,8 +87,9 @@ impl AuthServer {
};
tracing::info!("AUTH: accepted connection from {}", remote_addr);
let conn = tokio::spawn(NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error());
let conn = tokio::spawn(
NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(),
);
connections.push(conn);
}
@ -106,7 +105,7 @@ impl AuthServer {
struct NetLoop {
login: ArcLoginProvider,
stream: BufStream<TcpStream>,
stop: watch::Receiver<bool>,
stop: watch::Receiver<bool>,
state: State,
read_buf: Vec<u8>,
write_buf: BytesMut,
@ -197,82 +196,114 @@ enum State {
Init,
HandshakePart(Version),
HandshakeDone,
AuthPlainProgress {
id: u64,
},
AuthDone {
id: u64,
res: AuthRes
},
AuthPlainProgress { id: u64 },
AuthDone { id: u64, res: AuthRes },
}
const SERVER_MAJOR: u64 = 1;
const SERVER_MINOR: u64 = 2;
const EMPTY_AUTHZ: &[u8] = &[];
impl State {
async fn progress(&mut self, cmd: ClientCommand, login: &ArcLoginProvider) {
async fn try_auth_plain<'a>(&self, data: &'a [u8], login: &ArcLoginProvider) -> AuthRes {
// Check that we can extract user's login+pass
let (ubin, pbin) = match auth_plain(&data) {
Ok(([], (authz, user, pass))) if authz == user || authz == EMPTY_AUTHZ => (user, pass),
Ok(_) => {
tracing::error!("Impersonating user is not supported");
return AuthRes::Failed(None, None);
}
Err(e) => {
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
return AuthRes::Failed(None, None);
}
};
// Try to convert it to UTF-8
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
(Ok(u), Ok(p)) => (u, p),
_ => {
tracing::error!("Username or password contain invalid UTF-8 characters");
return AuthRes::Failed(None, None);
}
};
// Try to connect user
match login.login(user, password).await {
Ok(_) => AuthRes::Success(user.to_string()),
Err(e) => {
tracing::warn!(err=?e, "login failed");
AuthRes::Failed(Some(user.to_string()), None)
}
}
}
async fn progress(&mut self, cmd: ClientCommand, login: &ArcLoginProvider) {
let new_state = 'state: {
match (std::mem::replace(self, State::Error), cmd) {
(Self::Init, ClientCommand::Version(v)) => Self::HandshakePart(v),
(Self::HandshakePart(version), ClientCommand::Cpid(_cpid)) => {
if version.major != SERVER_MAJOR {
tracing::error!(client_major=version.major, server_major=SERVER_MAJOR, "Unsupported client major version");
break 'state Self::Error
}
Self::HandshakeDone
},
(Self::HandshakeDone { .. }, ClientCommand::Auth { id, mech, .. }) |
(Self::AuthDone { .. }, ClientCommand::Auth { id, mech, ..}) => {
if mech != Mechanism::Plain {
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
break 'state Self::AuthDone { id, res: AuthRes::Failed(None, None) }
tracing::error!(
client_major = version.major,
server_major = SERVER_MAJOR,
"Unsupported client major version"
);
break 'state Self::Error;
}
Self::AuthPlainProgress { id }
},
Self::HandshakeDone
}
(
Self::HandshakeDone { .. },
ClientCommand::Auth {
id, mech, options, ..
},
)
| (
Self::AuthDone { .. },
ClientCommand::Auth {
id, mech, options, ..
},
) => {
if mech != Mechanism::Plain {
tracing::error!(mechanism=?mech, "Unsupported Authentication Mechanism");
break 'state Self::AuthDone {
id,
res: AuthRes::Failed(None, None),
};
}
match options.last() {
Some(AuthOption::Resp(data)) => Self::AuthDone {
id,
res: self.try_auth_plain(&data, login).await,
},
_ => Self::AuthPlainProgress { id },
}
}
(Self::AuthPlainProgress { id }, ClientCommand::Cont { id: cid, data }) => {
// Check that ID matches
if cid != id {
tracing::error!(auth_id=id, cont_id=cid, "CONT id does not match AUTH id");
break 'state Self::AuthDone { id, res: AuthRes::Failed(None, None) }
tracing::error!(
auth_id = id,
cont_id = cid,
"CONT id does not match AUTH id"
);
break 'state Self::AuthDone {
id,
res: AuthRes::Failed(None, None),
};
}
// Check that we can extract user's login+pass
let (ubin, pbin) = match auth_plain(&data) {
Ok(([], ([], user, pass))) => (user, pass),
Ok(_) => {
tracing::error!("Impersonating user is not supported");
break 'state Self::AuthDone { id, res: AuthRes::Failed(None, None) }
}
Err(e) => {
tracing::error!(err=?e, "Could not parse the SASL PLAIN data chunk");
break 'state Self::AuthDone { id, res: AuthRes::Failed(None, None) }
},
};
// Try to convert it to UTF-8
let (user, password) = match (std::str::from_utf8(ubin), std::str::from_utf8(pbin)) {
(Ok(u), Ok(p)) => (u, p),
_ => {
tracing::error!("Username or password contain invalid UTF-8 characters");
break 'state Self::AuthDone { id, res: AuthRes::Failed(None, None) }
}
};
// Try to connect user
match login.login(user, password).await {
Ok(_) => Self::AuthDone { id, res: AuthRes::Success(user.to_string())},
Err(e) => {
tracing::warn!(err=?e, "login failed");
Self::AuthDone { id, res: AuthRes::Failed(Some(user.to_string()), None) }
}
Self::AuthDone {
id,
res: self.try_auth_plain(&data, login).await,
}
},
}
_ => {
tracing::error!("This command is not valid in this context");
Self::Error
},
}
}
};
tracing::debug!(state=?new_state, "Made progress");
@ -284,7 +315,10 @@ impl State {
match self {
Self::HandshakeDone { .. } => {
srv_cmd.push(ServerCommand::Version(Version { major: SERVER_MAJOR, minor: SERVER_MINOR }));
srv_cmd.push(ServerCommand::Version(Version {
major: SERVER_MAJOR,
minor: SERVER_MINOR,
}));
srv_cmd.push(ServerCommand::Mech {
kind: Mechanism::Plain,
@ -299,16 +333,34 @@ impl State {
srv_cmd.push(ServerCommand::Cookie(cookie));
srv_cmd.push(ServerCommand::Done);
},
}
Self::AuthPlainProgress { id } => {
srv_cmd.push(ServerCommand::Cont { id: *id, data: None });
},
Self::AuthDone { id, res: AuthRes::Success(user) } => {
srv_cmd.push(ServerCommand::Ok { id: *id, user_id: Some(user.to_string()), extra_parameters: vec![]});
},
Self::AuthDone { id, res: AuthRes::Failed(maybe_user, maybe_failcode) } => {
srv_cmd.push(ServerCommand::Fail { id: *id, user_id: maybe_user.clone(), code: maybe_failcode.clone(), extra_parameters: vec![]});
},
srv_cmd.push(ServerCommand::Cont {
id: *id,
data: None,
});
}
Self::AuthDone {
id,
res: AuthRes::Success(user),
} => {
srv_cmd.push(ServerCommand::Ok {
id: *id,
user_id: Some(user.to_string()),
extra_parameters: vec![],
});
}
Self::AuthDone {
id,
res: AuthRes::Failed(maybe_user, maybe_failcode),
} => {
srv_cmd.push(ServerCommand::Fail {
id: *id,
user_id: maybe_user.clone(),
code: maybe_failcode.clone(),
extra_parameters: vec![],
});
}
_ => (),
};
@ -316,7 +368,6 @@ impl State {
}
}
// -----------------------------------------------------------------
//
// DOVECOT AUTH TYPES
@ -329,7 +380,6 @@ enum Mechanism {
Login,
}
#[derive(Clone, Debug)]
enum AuthOption {
/// Unique session ID. Mainly used for logging.
@ -343,9 +393,9 @@ enum AuthOption {
/// Remote client port
RemotePort(u16),
/// When Dovecot proxy is used, the real_rip/real_port are the proxys IP/port and real_lip/real_lport are the backends IP/port where the proxy was connected to.
RealRemoteIp(String),
RealLocalIp(String),
RealLocalPort(u16),
RealRemoteIp(String),
RealLocalIp(String),
RealLocalPort(u16),
RealRemotePort(u16),
/// TLS SNI name
LocalName(String),
@ -380,8 +430,8 @@ enum AuthOption {
/// An unknown key
UnknownPair(String, Vec<u8>),
UnknownBool(Vec<u8>),
/// Initial response for authentication mechanism.
/// NOTE: This must be the last parameter. Everything after it is ignored.
/// Initial response for authentication mechanism.
/// NOTE: This must be the last parameter. Everything after it is ignored.
/// This is to avoid accidental security holes if user-given data is directly put to base64 string without filtering out tabs.
/// @FIXME: I don't understand this parameter
Resp(Vec<u8>),
@ -409,14 +459,13 @@ enum ClientCommand {
service: String,
/// All the optional parameters
options: Vec<AuthOption>,
},
Cont {
/// The <id> must match the <id> of the AUTH command.
id: u64,
/// Data that will be serialized to / deserialized from base64
data: Vec<u8>,
}
},
}
#[derive(Debug)]
@ -464,8 +513,8 @@ enum ServerCommand {
parameters: Vec<MechanismParameters>,
},
/// COOKIE returns connection-specific 128 bit cookie in hex. It must be given to REQUEST command. (Protocol v1.1+ / Dovecot v2.0+)
Cookie([u8;16]),
/// DONE finishes the handshake from server.
Cookie([u8; 16]),
/// DONE finishes the handshake from server.
Done,
Fail {
@ -478,7 +527,7 @@ enum ServerCommand {
id: u64,
data: Option<Vec<u8>>,
},
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
/// FAIL and OK may contain multiple unspecified parameters which authentication client may handle specially.
/// The only one specified here is user=<userid> parameter, which should always be sent if the userid is known.
Ok {
id: u64,
@ -493,26 +542,20 @@ enum ServerCommand {
//
// ------------------------------------------------------------------
use nom::{
IResult,
branch::alt,
error::{ErrorKind, Error},
character::complete::{tab, u64, u16},
bytes::complete::{is_not, tag, tag_no_case, take, take_while, take_while1},
multi::{many1, separated_list0},
combinator::{map, opt, recognize, value, rest},
sequence::{pair, preceded, tuple},
};
use base64::Engine;
use nom::{
branch::alt,
bytes::complete::{is_not, tag, tag_no_case, take, take_while, take_while1},
character::complete::{tab, u16, u64},
combinator::{map, opt, recognize, rest, value},
error::{Error, ErrorKind},
multi::{many1, separated_list0},
sequence::{pair, preceded, tuple},
IResult,
};
fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((
tag_no_case(b"VERSION"),
tab,
u64,
tab,
u64
));
let mut parser = tuple((tag_no_case(b"VERSION"), tab, u64, tab, u64));
let (input, (_, _, major, _, minor)) = parser(input)?;
Ok((input, ClientCommand::Version(Version { major, minor })))
@ -521,7 +564,7 @@ fn version_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
fn cpid_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
preceded(
pair(tag_no_case(b"CPID"), tab),
map(u64, |v| ClientCommand::Cpid(v))
map(u64, |v| ClientCommand::Cpid(v)),
)(input)
}
@ -541,10 +584,7 @@ fn is_esc<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
}
fn parameter<'a>(input: &'a [u8]) -> IResult<&'a [u8], &[u8]> {
recognize(many1(alt((
take_while1(is_not_tab_or_esc_or_lf),
is_esc
))))(input)
recognize(many1(alt((take_while1(is_not_tab_or_esc_or_lf), is_esc))))(input)
}
fn parameter_str(input: &[u8]) -> IResult<&[u8], String> {
@ -568,10 +608,7 @@ fn parameter_name(input: &[u8]) -> IResult<&[u8], String> {
}
fn service<'a>(input: &'a [u8]) -> IResult<&'a [u8], String> {
preceded(
tag_no_case("service="),
parameter_str
)(input)
preceded(tag_no_case("service="), parameter_str)(input)
}
fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
@ -583,31 +620,74 @@ fn auth_option<'a>(input: &'a [u8]) -> IResult<&'a [u8], AuthOption> {
value(ClientId, tag_no_case(b"client_id")),
value(NoLogin, tag_no_case(b"nologin")),
map(preceded(tag_no_case(b"session="), u64), |id| Session(id)),
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| LocalIp(ip)),
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| RemoteIp(ip)),
map(preceded(tag_no_case(b"lport="), u16), |port| LocalPort(port)),
map(preceded(tag_no_case(b"rport="), u16), |port| RemotePort(port)),
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| RealRemoteIp(ip)),
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| RealLocalIp(ip)),
map(preceded(tag_no_case(b"real_lport="), u16), |port| RealLocalPort(port)),
map(preceded(tag_no_case(b"real_rport="), u16), |port| RealRemotePort(port)),
map(preceded(tag_no_case(b"lip="), parameter_str), |ip| {
LocalIp(ip)
}),
map(preceded(tag_no_case(b"rip="), parameter_str), |ip| {
RemoteIp(ip)
}),
map(preceded(tag_no_case(b"lport="), u16), |port| {
LocalPort(port)
}),
map(preceded(tag_no_case(b"rport="), u16), |port| {
RemotePort(port)
}),
map(preceded(tag_no_case(b"real_rip="), parameter_str), |ip| {
RealRemoteIp(ip)
}),
map(preceded(tag_no_case(b"real_lip="), parameter_str), |ip| {
RealLocalIp(ip)
}),
map(preceded(tag_no_case(b"real_lport="), u16), |port| {
RealLocalPort(port)
}),
map(preceded(tag_no_case(b"real_rport="), u16), |port| {
RealRemotePort(port)
}),
)),
alt((
map(preceded(tag_no_case(b"local_name="), parameter_str), |name| LocalName(name)),
map(preceded(tag_no_case(b"forward_views="), parameter), |views| ForwardViews(views.into())),
map(preceded(tag_no_case(b"secured="), parameter_str), |info| Secured(Some(info))),
map(
preceded(tag_no_case(b"local_name="), parameter_str),
|name| LocalName(name),
),
map(
preceded(tag_no_case(b"forward_views="), parameter),
|views| ForwardViews(views.into()),
),
map(preceded(tag_no_case(b"secured="), parameter_str), |info| {
Secured(Some(info))
}),
value(Secured(None), tag_no_case(b"secured")),
value(CertUsername, tag_no_case(b"cert_username")),
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| Transport(ts)),
map(preceded(tag_no_case(b"tls_cipher="), parameter_str), |cipher| TlsCipher(cipher)),
map(preceded(tag_no_case(b"tls_cipher_bits="), parameter_str), |bits| TlsCipherBits(bits)),
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| TlsPfs(pfs)),
map(preceded(tag_no_case(b"tls_protocol="), parameter_str), |proto| TlsProtocol(proto)),
map(preceded(tag_no_case(b"valid-client-cert="), parameter_str), |cert| ValidClientCert(cert)),
map(preceded(tag_no_case(b"transport="), parameter_str), |ts| {
Transport(ts)
}),
map(
preceded(tag_no_case(b"tls_cipher="), parameter_str),
|cipher| TlsCipher(cipher),
),
map(
preceded(tag_no_case(b"tls_cipher_bits="), parameter_str),
|bits| TlsCipherBits(bits),
),
map(preceded(tag_no_case(b"tls_pfs="), parameter_str), |pfs| {
TlsPfs(pfs)
}),
map(
preceded(tag_no_case(b"tls_protocol="), parameter_str),
|proto| TlsProtocol(proto),
),
map(
preceded(tag_no_case(b"valid-client-cert="), parameter_str),
|cert| ValidClientCert(cert),
),
)),
alt((
map(preceded(tag_no_case(b"resp="), base64), |data| Resp(data)),
map(tuple((parameter_name, tag(b"="), parameter)), |(n, _, v)| UnknownPair(n, v.into())),
map(
tuple((parameter_name, tag(b"="), parameter)),
|(n, _, v)| UnknownPair(n, v.into()),
),
map(parameter, |v| UnknownBool(v.into())),
)),
))(input)
@ -622,13 +702,20 @@ fn auth_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
mechanism,
tab,
service,
map(
opt(preceded(tab, separated_list0(tab, auth_option))),
|o| o.unwrap_or(vec![])
),
map(opt(preceded(tab, separated_list0(tab, auth_option))), |o| {
o.unwrap_or(vec![])
}),
));
let (input, (_, _, id, _, mech, _, service, options)) = parser(input)?;
Ok((input, ClientCommand::Auth { id, mech, service, options }))
Ok((
input,
ClientCommand::Auth {
id,
mech,
service,
options,
},
))
}
fn is_base64_core(c: u8) -> bool {
@ -644,10 +731,7 @@ fn is_base64_pad(c: u8) -> bool {
}
fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
let (input, (b64, _)) = tuple((
take_while1(is_base64_core),
take_while(is_base64_pad),
))(input)?;
let (input, (b64, _)) = tuple((take_while1(is_base64_core), take_while(is_base64_pad)))(input)?;
let data = base64::engine::general_purpose::STANDARD_NO_PAD
.decode(b64)
@ -657,26 +741,15 @@ fn base64(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
}
/// @FIXME Dovecot does not say if base64 content must be padded or not
fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((
tag_no_case(b"CONT"),
tab,
u64,
tab,
base64
));
fn cont_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
let mut parser = tuple((tag_no_case(b"CONT"), tab, u64, tab, base64));
let (input, (_, _, id, _, data)) = parser(input)?;
Ok((input, ClientCommand::Cont { id, data }))
}
fn client_command<'a>(input: &'a [u8]) -> IResult<&'a [u8], ClientCommand> {
alt((
version_command,
cpid_command,
auth_command,
cont_command,
))(input)
alt((version_command, cpid_command, auth_command, cont_command))(input)
}
/*
@ -698,7 +771,13 @@ fn not_null(c: u8) -> bool {
// impersonated user, login, password
fn auth_plain<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&'a [u8], &'a [u8], &'a [u8])> {
map(
tuple((take_while(not_null), take(1usize), take_while(not_null), take(1usize), rest)),
tuple((
take_while(not_null),
take(1usize),
take_while(not_null),
take(1usize),
rest,
)),
|(imp, _, user, _, pass)| (imp, user, pass),
)(input)
}
@ -746,7 +825,6 @@ impl Encode for MechanismParameters {
}
}
impl Encode for FailCode {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
@ -762,33 +840,32 @@ impl Encode for FailCode {
impl Encode for ServerCommand {
fn encode(&self, out: &mut BytesMut) -> Result<()> {
match self {
Self::Version (Version { major, minor }) => {
Self::Version(Version { major, minor }) => {
out.put(&b"VERSION"[..]);
tab_enc(out);
out.put(major.to_string().as_bytes());
tab_enc(out);
out.put(minor.to_string().as_bytes());
lf_enc(out);
},
}
Self::Spid(pid) => {
out.put(&b"SPID"[..]);
tab_enc(out);
out.put(pid.to_string().as_bytes());
lf_enc(out);
},
}
Self::Cuid(pid) => {
out.put(&b"CUID"[..]);
tab_enc(out);
out.put(pid.to_string().as_bytes());
lf_enc(out);
},
}
Self::Cookie(cval) => {
out.put(&b"COOKIE"[..]);
tab_enc(out);
out.put(hex::encode(cval).as_bytes());
out.put(hex::encode(cval).as_bytes());
lf_enc(out);
},
}
Self::Mech { kind, parameters } => {
out.put(&b"MECH"[..]);
tab_enc(out);
@ -798,11 +875,11 @@ impl Encode for ServerCommand {
p.encode(out)?;
}
lf_enc(out);
},
}
Self::Done => {
out.put(&b"DONE"[..]);
lf_enc(out);
},
}
Self::Cont { id, data } => {
out.put(&b"CONT"[..]);
tab_enc(out);
@ -813,8 +890,12 @@ impl Encode for ServerCommand {
out.put(b64.as_bytes());
}
lf_enc(out);
},
Self::Ok { id, user_id, extra_parameters } => {
}
Self::Ok {
id,
user_id,
extra_parameters,
} => {
out.put(&b"OK"[..]);
tab_enc(out);
out.put(id.to_string().as_bytes());
@ -828,8 +909,13 @@ impl Encode for ServerCommand {
out.put(&p[..]);
}
lf_enc(out);
},
Self::Fail {id, user_id, code, extra_parameters } => {
}
Self::Fail {
id,
user_id,
code,
extra_parameters,
} => {
out.put(&b"FAIL"[..]);
tab_enc(out);
out.put(id.to_string().as_bytes());
@ -848,7 +934,7 @@ impl Encode for ServerCommand {
out.put(&p[..]);
}
lf_enc(out);
},
}
}
Ok(())
}

View File

@ -1,5 +1,5 @@
use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier};
use imap_codec::imap_types::core::NonEmptyVec;
use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::extensions::enable::{CapabilityEnable, Utf8Kind};
use imap_codec::imap_types::response::Capability;
use std::collections::HashSet;
@ -49,7 +49,7 @@ impl Default for ServerCapability {
}
impl ServerCapability {
pub fn to_vec(&self) -> NonEmptyVec<Capability<'static>> {
pub fn to_vec(&self) -> Vec1<Capability<'static>> {
self.0
.iter()
.map(|v| v.clone())

View File

@ -6,7 +6,7 @@ use anyhow::{anyhow, bail, Result};
use imap_codec::imap_types::command::{
Command, CommandBody, ListReturnItem, SelectExamineModifier,
};
use imap_codec::imap_types::core::{Atom, Literal, NonEmptyVec, QuotedChar};
use imap_codec::imap_types::core::{Atom, Literal, QuotedChar, Vec1};
use imap_codec::imap_types::datetime::DateTime;
use imap_codec::imap_types::extensions::enable::CapabilityEnable;
use imap_codec::imap_types::flag::{Flag, FlagNameAttribute};
@ -17,10 +17,10 @@ use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName};
use crate::imap::capability::{ClientCapability, ServerCapability};
use crate::imap::command::{anystate, MailboxName};
use crate::imap::flow;
use crate::imap::mailbox_view::MailboxView;
use crate::imap::mailbox_view::{MailboxView, UpdateParameters};
use crate::imap::response::Response;
use crate::imap::Body;
use crate::mail::mailbox::Mailbox;
use crate::mail::uidindex::*;
use crate::mail::user::{User, MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW};
use crate::mail::IMF;
@ -549,6 +549,8 @@ impl<'a> AuthenticatedContext<'a> {
))
}
//@FIXME we should write a specific version for the "selected" state
//that returns some unsollicited responses
async fn append(
self,
mailbox: &MailboxCodec<'a>,
@ -558,7 +560,7 @@ impl<'a> AuthenticatedContext<'a> {
) -> Result<(Response<'static>, flow::Transition)> {
let append_tag = self.req.tag.clone();
match self.append_internal(mailbox, flags, date, message).await {
Ok((_mb, uidvalidity, uid, _modseq)) => Ok((
Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok((
Response::build()
.tag(append_tag)
.message("APPEND completed")
@ -580,7 +582,7 @@ impl<'a> AuthenticatedContext<'a> {
fn enable(
self,
cap_enable: &NonEmptyVec<CapabilityEnable<'static>>,
cap_enable: &Vec1<CapabilityEnable<'static>>,
) -> Result<(Response<'static>, flow::Transition)> {
let mut response_builder = Response::build().to_req(self.req);
let capabilities = self.client_capabilities.try_enable(cap_enable.as_ref());
@ -593,13 +595,14 @@ impl<'a> AuthenticatedContext<'a> {
))
}
//@FIXME should be refactored and integrated to the mailbox view
pub(crate) async fn append_internal(
self,
mailbox: &MailboxCodec<'a>,
flags: &[Flag<'a>],
date: &Option<DateTime>,
message: &Literal<'a>,
) -> Result<(Arc<Mailbox>, ImapUidvalidity, ImapUid, ModSeq)> {
) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> {
let name: &str = MailboxName(mailbox).try_into()?;
let mb_opt = self.user.open_mailbox(&name).await?;
@ -607,6 +610,7 @@ impl<'a> AuthenticatedContext<'a> {
Some(mb) => mb,
None => bail!("Mailbox does not exist"),
};
let mut view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await;
if date.is_some() {
tracing::warn!("Cannot set date when appending message");
@ -617,9 +621,11 @@ impl<'a> AuthenticatedContext<'a> {
let flags = flags.iter().map(|x| x.to_string()).collect::<Vec<_>>();
// TODO: filter allowed flags? ping @Quentin
let (uidvalidity, uid, modseq) = mb.append(msg, None, &flags[..]).await?;
let (uidvalidity, uid, modseq) =
view.internal.mailbox.append(msg, None, &flags[..]).await?;
//let unsollicited = view.update(UpdateParameters::default()).await?;
Ok((mb, uidvalidity, uid, modseq))
Ok((view, uidvalidity, uid, modseq))
}
}

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use anyhow::Result;
use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier};
use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::core::{Charset, Vec1};
use imap_codec::imap_types::fetch::MacroOrMessageDataItemNames;
use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType};
use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec;
@ -54,11 +54,15 @@ pub async fn dispatch<'a>(
ctx.fetch(sequence_set, macro_or_item_names, modifiers, uid)
.await
}
//@FIXME SearchKey::And is a legacy hack, should be refactored
CommandBody::Search {
charset,
criteria,
uid,
} => ctx.search(charset, criteria, uid).await,
} => {
ctx.search(charset, &SearchKey::And(criteria.clone()), uid)
.await
}
CommandBody::Expunge {
// UIDPLUS (rfc4315)
uid_sequence_set,
@ -88,15 +92,6 @@ pub async fn dispatch<'a>(
// UNSELECT extension (rfc3691)
CommandBody::Unselect => ctx.unselect().await,
// IDLE extension (rfc2177)
CommandBody::Idle => Ok((
Response::build()
.to_req(ctx.req)
.message("DUMMY command due to anti-pattern in the code")
.ok()?,
flow::Transition::Idle(ctx.req.tag.clone(), tokio::sync::Notify::new()),
)),
// In selected mode, we fallback to authenticated when needed
_ => {
authenticated::dispatch(authenticated::AuthenticatedContext {

View File

@ -1,11 +1,12 @@
use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;
use imap_codec::imap_types::core::Tag;
use tokio::sync::Notify;
use crate::imap::mailbox_view::MailboxView;
use crate::mail::user::User;
use imap_codec::imap_types::core::Tag;
#[derive(Debug)]
pub enum Error {
@ -31,6 +32,14 @@ pub enum State {
),
Logout,
}
impl State {
pub fn notify(&self) -> Option<Arc<Notify>> {
match self {
Self::Idle(_, _, _, _, anotif) => Some(anotif.clone()),
_ => None,
}
}
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use State::*;

View File

@ -4,9 +4,9 @@ use std::sync::Arc;
use anyhow::{anyhow, Error, Result};
use futures::stream::{FuturesOrdered, StreamExt};
use futures::stream::{StreamExt, TryStreamExt};
use imap_codec::imap_types::core::Charset;
use imap_codec::imap_types::core::{Charset, Vec1};
use imap_codec::imap_types::fetch::MessageDataItem;
use imap_codec::imap_types::flag::{Flag, FlagFetch, FlagPerm, StoreResponse, StoreType};
use imap_codec::imap_types::response::{Code, CodeOther, Data, Status};
@ -362,46 +362,36 @@ impl MailboxView {
.iter()
.map(|midx| midx.uuid)
.collect::<Vec<_>>();
let query_result = self.internal.query(&uuids, query_scope).fetch().await?;
// [3/6] Derive an IMAP-specific view from the results, apply the filters
let views = query_result
.iter()
.zip(mail_idx_list.into_iter())
.map(|(qr, midx)| MailView::new(qr, midx))
.collect::<Result<Vec<_>, _>>()?;
let query = self.internal.query(&uuids, query_scope);
//let query_result = self.internal.query(&uuids, query_scope).fetch().await?;
// [4/6] Apply the IMAP transformation, bubble up any error
// We get 2 results:
// - The one we send to the client
// - The \Seen flags we must set internally
let (flag_mgmt, imap_ret): (Vec<_>, Vec<_>) = views
.iter()
.map(|mv| mv.filter(&ap).map(|(body, seen)| ((mv, seen), body)))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.unzip();
// [5/6] Register the \Seen flags
flag_mgmt
.iter()
.filter(|(_mv, seen)| matches!(seen, SeenFlag::MustAdd))
.map(|(mv, _seen)| async move {
let seen_flag = Flag::Seen.to_string();
self.internal
.mailbox
.add_flags(*mv.query_result.uuid(), &[seen_flag])
.await?;
Ok::<_, anyhow::Error>(())
let query_stream = query
.fetch()
.zip(futures::stream::iter(mail_idx_list))
// [3/6] Derive an IMAP-specific view from the results, apply the filters
.map(|(maybe_qr, midx)| match maybe_qr {
Ok(qr) => Ok((MailView::new(&qr, midx)?.filter(&ap)?, midx)),
Err(e) => Err(e),
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<_, _>>()?;
// [4/6] Apply the IMAP transformation
.then(|maybe_ret| async move {
let ((body, seen), midx) = maybe_ret?;
// [5/6] Register the \Seen flags
if matches!(seen, SeenFlag::MustAdd) {
let seen_flag = Flag::Seen.to_string();
self.internal
.mailbox
.add_flags(midx.uuid, &[seen_flag])
.await?;
}
Ok::<_, anyhow::Error>(body)
});
// [6/6] Build the final result that will be sent to the client.
Ok(imap_ret)
query_stream.try_collect().await
}
/// A naive search implementation...
@ -423,39 +413,54 @@ impl MailboxView {
// 3. Filter the selection based on the ID / UID / Flags
let (kept_idx, to_fetch) = crit.filter_on_idx(&selection);
// 4. Fetch additional info about the emails
// 4.a Fetch additional info about the emails
let query_scope = crit.query_scope();
let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::<Vec<_>>();
let query_result = self.internal.query(&uuids, query_scope).fetch().await?;
let query = self.internal.query(&uuids, query_scope);
// 5. If needed, filter the selection based on the body
let kept_query = crit.filter_on_query(&to_fetch, &query_result)?;
// 4.b We don't want to keep all data in memory, so we do the computing in a stream
let query_stream = query
.fetch()
.zip(futures::stream::iter(&to_fetch))
// 5.a Build a mailview with the body, might fail with an error
// 5.b If needed, filter the selection based on the body, but keep the errors
// 6. Drop the query+mailbox, keep only the mail index
// Here we release a lot of memory, this is the most important part ^^
.filter_map(|(maybe_qr, midx)| {
let r = match maybe_qr {
Ok(qr) => match MailView::new(&qr, midx).map(|mv| crit.is_keep_on_query(&mv)) {
Ok(true) => Some(Ok(*midx)),
Ok(_) => None,
Err(e) => Some(Err(e)),
},
Err(e) => Some(Err(e)),
};
futures::future::ready(r)
});
// 6. Format the result according to the client's taste:
// either return UID or ID.
let final_selection = kept_idx.iter().chain(kept_query.iter());
let selection_fmt = match uid {
true => final_selection.map(|in_idx| in_idx.uid).collect(),
_ => final_selection.map(|in_idx| in_idx.i).collect(),
};
// 7. Chain both streams (part resolved from index, part resolved from metadata+body)
let main_stream = futures::stream::iter(kept_idx)
.map(Ok)
.chain(query_stream)
.map_ok(|idx| match uid {
true => (idx.uid, idx.modseq),
_ => (idx.i, idx.modseq),
});
// 7. Add the modseq entry if needed
let is_modseq = crit.is_modseq();
let maybe_modseq = match is_modseq {
true => {
let final_selection = kept_idx.iter().chain(kept_query.iter());
final_selection
.map(|in_idx| in_idx.modseq)
.max()
.map(|r| NonZeroU64::try_from(r))
.transpose()?
}
// 8. Do the actual computation
let internal_result: Vec<_> = main_stream.try_collect().await?;
let (selection, modseqs): (Vec<_>, Vec<_>) = internal_result.into_iter().unzip();
// 9. Aggregate the maximum modseq value
let maybe_modseq = match crit.is_modseq() {
true => modseqs.into_iter().max(),
_ => None,
};
// 10. Return the final result
Ok((
vec![Body::Data(Data::Search(selection_fmt, maybe_modseq))],
is_modseq,
vec![Body::Data(Data::Search(selection, maybe_modseq))],
maybe_modseq.is_some(),
))
}
@ -626,7 +631,7 @@ impl MailboxView {
mod tests {
use super::*;
use imap_codec::encode::Encoder;
use imap_codec::imap_types::core::NonEmptyVec;
use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::fetch::Section;
use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName};
use imap_codec::imap_types::response::Response;
@ -746,7 +751,7 @@ mod tests {
let test_repr = Response::Data(Data::Fetch {
seq: NonZeroU32::new(1).unwrap(),
items: NonEmptyVec::from(MessageDataItem::Body(mime_view::bodystructure(
items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure(
&message.child,
false,
)?)),

View File

@ -8,7 +8,7 @@ use imap_codec::imap_types::body::{
BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData,
SpecificFields,
};
use imap_codec::imap_types::core::{AString, IString, NString, NonEmptyVec};
use imap_codec::imap_types::core::{AString, IString, NString, Vec1};
use imap_codec::imap_types::fetch::{Part as FetchPart, Section as FetchSection};
use eml_codec::{
@ -141,8 +141,8 @@ impl<'a> NodeMime<'a> {
enum SubsettedSection<'a> {
Part,
Header,
HeaderFields(&'a NonEmptyVec<AString<'a>>),
HeaderFieldsNot(&'a NonEmptyVec<AString<'a>>),
HeaderFields(&'a Vec1<AString<'a>>),
HeaderFieldsNot(&'a Vec1<AString<'a>>),
Text,
Mime,
}
@ -238,7 +238,7 @@ impl<'a> SelectedMime<'a> {
/// case-insensitive but otherwise exact.
fn header_fields(
&self,
fields: &'a NonEmptyVec<AString<'a>>,
fields: &'a Vec1<AString<'a>>,
invert: bool,
) -> Result<ExtractedFull<'a>> {
// Build a lowercase ascii hashset with the fields to fetch
@ -398,8 +398,8 @@ impl<'a> NodeMult<'a> {
.filter_map(|inner| NodeMime(&inner).structure(is_ext).ok())
.collect::<Vec<_>>();
NonEmptyVec::validate(&inner_bodies)?;
let bodies = NonEmptyVec::unvalidated(inner_bodies);
Vec1::validate(&inner_bodies)?;
let bodies = Vec1::unvalidated(inner_bodies);
Ok(BodyStructure::Multi {
bodies,

View File

@ -15,7 +15,7 @@ mod session;
use std::net::SocketAddr;
use anyhow::{bail, Result};
use anyhow::{anyhow, bail, Context, Result};
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::net::TcpListener;
@ -26,8 +26,8 @@ use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Respons
use imap_codec::imap_types::{core::Text, response::Greeting};
use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions};
use imap_flow::stream::AnyStream;
use tokio_rustls::TlsAcceptor;
use rustls_pemfile::{certs, private_key};
use tokio_rustls::TlsAcceptor;
use crate::config::{ImapConfig, ImapUnsecureConfig};
use crate::imap::capability::ServerCapability;
@ -53,8 +53,14 @@ struct ClientContext {
}
pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result<Server> {
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(config.certs)?)).collect::<Result<Vec<_>, _>>()?;
let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(config.key)?))?.unwrap();
let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open(
config.certs,
)?))
.collect::<Result<Vec<_>, _>>()?;
let loaded_key = private_key(&mut std::io::BufReader::new(std::fs::File::open(
config.key,
)?))?
.unwrap();
let tls_config = rustls::ServerConfig::builder()
.with_no_client_auth()
@ -109,7 +115,7 @@ impl Server {
}
};
AnyStream::new(stream)
},
}
None => AnyStream::new(socket),
};
@ -135,11 +141,8 @@ use std::sync::Arc;
use tokio::sync::mpsc::*;
use tokio::sync::Notify;
use tokio_util::bytes::BytesMut;
enum LoopMode {
Quit,
Interactive,
Idle(BytesMut, Arc<Notify>),
}
const PIPELINABLE_COMMANDS: usize = 64;
// @FIXME a full refactor of this part of the code will be needed sooner or later
struct NetLoop {
@ -153,7 +156,7 @@ impl NetLoop {
async fn handler(ctx: ClientContext, sock: AnyStream) {
let addr = ctx.addr.clone();
let nl = match Self::new(ctx, sock).await {
let mut nl = match Self::new(ctx, sock).await {
Ok(nl) => {
tracing::debug!(addr=?addr, "netloop successfully initialized");
nl
@ -175,15 +178,15 @@ impl NetLoop {
}
async fn new(ctx: ClientContext, sock: AnyStream) -> Result<Self> {
let mut opts = ServerFlowOptions::default();
opts.crlf_relaxed = false;
opts.literal_accept_text = Text::unvalidated("OK");
opts.literal_reject_text = Text::unvalidated("Literal rejected");
// Send greeting
let (server, _) = ServerFlow::send_greeting(
sock,
ServerFlowOptions {
crlf_relaxed: false,
literal_accept_text: Text::unvalidated("OK"),
literal_reject_text: Text::unvalidated("Literal rejected"),
..ServerFlowOptions::default()
},
opts,
Greeting::ok(
Some(Code::Capability(ctx.server_capabilities.to_vec())),
"Aerogramme",
@ -193,7 +196,7 @@ impl NetLoop {
.await?;
// Start a mailbox session in background
let (cmd_tx, cmd_rx) = mpsc::channel::<Request>(3);
let (cmd_tx, cmd_rx) = mpsc::channel::<Request>(PIPELINABLE_COMMANDS);
let (resp_tx, resp_rx) = mpsc::unbounded_channel::<ResponseOrIdle>();
tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx));
@ -231,83 +234,115 @@ impl NetLoop {
tracing::info!("runner is quitting");
}
async fn core(mut self) -> Result<()> {
let mut mode = LoopMode::Interactive;
async fn core(&mut self) -> Result<()> {
let mut maybe_idle: Option<Arc<Notify>> = None;
loop {
mode = match mode {
LoopMode::Interactive => self.interactive_mode().await?,
LoopMode::Idle(buff, stop) => self.idle_mode(buff, stop).await?,
LoopMode::Quit => break,
}
tokio::select! {
// Managing imap_flow stuff
srv_evt = self.server.progress() => match srv_evt? {
ServerFlowEvent::ResponseSent { handle: _handle, response } => {
match response {
Response::Status(Status::Bye(_)) => return Ok(()),
_ => tracing::trace!("sent to {} content {:?}", self.ctx.addr, response),
}
},
ServerFlowEvent::CommandReceived { command } => {
match self.cmd_tx.try_send(Request::ImapCommand(command)) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
self.server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr);
}
_ => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
}
}
},
ServerFlowEvent::IdleCommandReceived { tag } => {
match self.cmd_tx.try_send(Request::IdleStart(tag)) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
self.server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr);
}
_ => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
}
}
}
ServerFlowEvent::IdleDoneReceived => {
tracing::trace!("client sent DONE and want to stop IDLE");
maybe_idle.ok_or(anyhow!("Received IDLE done but not idling currently"))?.notify_one();
maybe_idle = None;
}
flow => {
self.server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap());
tracing::error!("session task exited for {:?} due to unsupported flow {:?}", self.ctx.addr, flow);
}
},
// Managing response generated by Aerogramme
maybe_msg = self.resp_rx.recv() => match maybe_msg {
Some(ResponseOrIdle::Response(response)) => {
tracing::trace!("Interactive, server has a response for the client");
for body_elem in response.body.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
Body::Status(s) => self.server.enqueue_status(s),
};
}
self.server.enqueue_status(response.completion);
},
Some(ResponseOrIdle::IdleAccept(stop)) => {
tracing::trace!("Interactive, server agreed to switch in idle mode");
let cr = CommandContinuationRequest::basic(None, "Idling")?;
self.server.idle_accept(cr).or(Err(anyhow!("refused continuation for idle accept")))?;
self.cmd_tx.try_send(Request::IdlePoll)?;
if maybe_idle.is_some() {
bail!("Can't start IDLE if already idling");
}
maybe_idle = Some(stop);
},
Some(ResponseOrIdle::IdleEvent(elems)) => {
tracing::trace!("server imap session has some change to communicate to the client");
for body_elem in elems.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
Body::Status(s) => self.server.enqueue_status(s),
};
}
self.cmd_tx.try_send(Request::IdlePoll)?;
},
Some(ResponseOrIdle::IdleReject(response)) => {
tracing::trace!("inform client that session rejected idle");
self.server
.idle_reject(response.completion)
.or(Err(anyhow!("wrong reject command")))?;
},
None => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
},
Some(_) => unreachable!(),
},
// When receiving a CTRL+C
_ = self.ctx.must_exit.changed() => {
tracing::trace!("Interactive, CTRL+C, exiting");
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
},
};
}
Ok(())
}
async fn interactive_mode(&mut self) -> Result<LoopMode> {
tokio::select! {
// Managing imap_flow stuff
srv_evt = self.server.progress() => match srv_evt? {
ServerFlowEvent::ResponseSent { handle: _handle, response } => {
match response {
Response::Status(Status::Bye(_)) => return Ok(LoopMode::Quit),
_ => tracing::trace!("sent to {} content {:?}", self.ctx.addr, response),
}
},
ServerFlowEvent::CommandReceived { command } => {
match self.cmd_tx.try_send(Request::ImapCommand(command)) {
Ok(_) => (),
Err(mpsc::error::TrySendError::Full(_)) => {
self.server.enqueue_status(Status::bye(None, "Too fast").unwrap());
tracing::error!("client {:?} is sending commands too fast, closing.", self.ctx.addr);
}
_ => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
}
}
},
flow => {
self.server.enqueue_status(Status::bye(None, "Unsupported server flow event").unwrap());
tracing::error!("session task exited for {:?} due to unsupported flow {:?}", self.ctx.addr, flow);
}
},
// Managing response generated by Aerogramme
maybe_msg = self.resp_rx.recv() => match maybe_msg {
Some(ResponseOrIdle::Response(response)) => {
for body_elem in response.body.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
Body::Status(s) => self.server.enqueue_status(s),
};
}
self.server.enqueue_status(response.completion);
},
Some(ResponseOrIdle::StartIdle(stop)) => {
let cr = CommandContinuationRequest::basic(None, "Idling")?;
self.server.enqueue_continuation(cr);
self.cmd_tx.try_send(Request::Idle)?;
return Ok(LoopMode::Idle(BytesMut::new(), stop))
},
None => {
self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap());
tracing::error!("session task exited for {:?}, quitting", self.ctx.addr);
},
Some(_) => unreachable!(),
},
// When receiving a CTRL+C
_ = self.ctx.must_exit.changed() => {
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
},
};
Ok(LoopMode::Interactive)
}
/*
async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc<Notify>) -> Result<LoopMode> {
// Flush send
loop {
tracing::trace!("flush server send");
match self.server.progress_send().await? {
Some(..) => continue,
None => break,
@ -319,6 +354,7 @@ impl NetLoop {
maybe_msg = self.resp_rx.recv() => match maybe_msg {
// Session decided idle is terminated
Some(ResponseOrIdle::Response(response)) => {
tracing::trace!("server imap session said idle is done, sending response done, switching to interactive");
for body_elem in response.body.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
@ -330,6 +366,7 @@ impl NetLoop {
},
// Session has some information for user
Some(ResponseOrIdle::IdleEvent(elems)) => {
tracing::trace!("server imap session has some change to communicate to the client");
for body_elem in elems.into_iter() {
let _handle = match body_elem {
Body::Data(d) => self.server.enqueue_data(d),
@ -352,16 +389,21 @@ impl NetLoop {
},
// User is trying to interact with us
_read_client_bytes = self.server.stream.read(&mut buff) => {
read_client_result = self.server.stream.read(&mut buff) => {
let _bytes_read = read_client_result?;
use imap_codec::decode::Decoder;
let codec = imap_codec::IdleDoneCodec::new();
tracing::trace!("client sent some data for the server IMAP session");
match codec.decode(&buff) {
Ok(([], imap_codec::imap_types::extensions::idle::IdleDone)) => {
// Session will be informed that it must stop idle
// It will generate the "done" message and change the loop mode
tracing::trace!("client sent DONE and want to stop IDLE");
stop.notify_one()
},
Err(_) => (),
Err(_) => {
tracing::trace!("Unable to decode DONE, maybe not enough data were sent?");
},
_ => bail!("Client sent data after terminating the continuation without waiting for the server. This is an unsupported behavior and bug in Aerogramme, quitting."),
};
@ -370,9 +412,10 @@ impl NetLoop {
// When receiving a CTRL+C
_ = self.ctx.must_exit.changed() => {
tracing::trace!("CTRL+C sent, aborting IDLE for this session");
self.server.enqueue_status(Status::bye(None, "Server is being shutdown").unwrap());
return Ok(LoopMode::Interactive)
},
};
}
}*/
}

View File

@ -1,7 +1,9 @@
use imap_codec::imap_types::command::Command;
use imap_codec::imap_types::core::Tag;
#[derive(Debug)]
pub enum Request {
ImapCommand(Command<'static>),
Idle,
IdleStart(Tag<'static>),
IdlePoll,
}

View File

@ -118,6 +118,7 @@ impl<'a> Response<'a> {
#[derive(Debug)]
pub enum ResponseOrIdle {
Response(Response<'static>),
StartIdle(Arc<Notify>),
IdleAccept(Arc<Notify>),
IdleReject(Response<'static>),
IdleEvent(Vec<Body<'static>>),
}

View File

@ -1,13 +1,12 @@
use std::num::{NonZeroU32, NonZeroU64};
use anyhow::Result;
use imap_codec::imap_types::core::NonEmptyVec;
use imap_codec::imap_types::core::Vec1;
use imap_codec::imap_types::search::{MetadataItemSearch, SearchKey};
use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet};
use crate::imap::index::MailIndex;
use crate::imap::mail_view::MailView;
use crate::mail::query::{QueryResult, QueryScope};
use crate::mail::query::QueryScope;
pub enum SeqType {
Undefined,
@ -49,7 +48,7 @@ impl<'a> Criteria<'a> {
let mut new_vec = base.0.into_inner();
new_vec.extend_from_slice(ext.0.as_ref());
let seq = SequenceSet(
NonEmptyVec::try_from(new_vec)
Vec1::try_from(new_vec)
.expect("merging non empty vec lead to non empty vec"),
);
(seq, x)
@ -145,22 +144,6 @@ impl<'a> Criteria<'a> {
(to_keep, to_fetch)
}
pub fn filter_on_query<'b>(
&self,
midx_list: &[&'b MailIndex<'b>],
query_result: &'b Vec<QueryResult>,
) -> Result<Vec<&'b MailIndex<'b>>> {
Ok(midx_list
.iter()
.zip(query_result.iter())
.map(|(midx, qr)| MailView::new(qr, midx))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|mail_view| self.is_keep_on_query(mail_view))
.map(|mail_view| mail_view.in_idx)
.collect())
}
// ----
/// Here we are doing a partial filtering: we do not have access
@ -213,7 +196,7 @@ impl<'a> Criteria<'a> {
/// the email, as body(x) might be false. So we need to check it. But as seqid(x) is true,
/// we could simplify the request to just body(x) and truncate the first OR. Today, we are
/// not doing that, and thus we reevaluate everything.
fn is_keep_on_query(&self, mail_view: &MailView) -> bool {
pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool {
use SearchKey::*;
match self.0 {
// Combinator logic

View File

@ -4,8 +4,8 @@ use crate::imap::flow;
use crate::imap::request::Request;
use crate::imap::response::{Response, ResponseOrIdle};
use crate::login::ArcLoginProvider;
use anyhow::{anyhow, bail, Result};
use imap_codec::imap_types::command::Command;
use anyhow::{anyhow, bail, Context, Result};
use imap_codec::imap_types::{command::Command, core::Tag};
//-----
pub struct Instance {
@ -27,13 +27,48 @@ impl Instance {
pub async fn request(&mut self, req: Request) -> ResponseOrIdle {
match req {
Request::Idle => self.idle().await,
Request::IdleStart(tag) => self.idle_init(tag),
Request::IdlePoll => self.idle_poll().await,
Request::ImapCommand(cmd) => self.command(cmd).await,
}
}
pub async fn idle(&mut self) -> ResponseOrIdle {
match self.idle_happy().await {
pub fn idle_init(&mut self, tag: Tag<'static>) -> ResponseOrIdle {
// Build transition
//@FIXME the notifier should be hidden inside the state and thus not part of the transition!
let transition = flow::Transition::Idle(tag.clone(), tokio::sync::Notify::new());
// Try to apply the transition and get the stop notifier
let maybe_stop = self
.state
.apply(transition)
.context("IDLE transition failed")
.and_then(|_| {
self.state
.notify()
.ok_or(anyhow!("IDLE state has no Notify object"))
});
// Build an appropriate response
match maybe_stop {
Ok(stop) => ResponseOrIdle::IdleAccept(stop),
Err(e) => {
tracing::error!(err=?e, "unable to init idle due to a transition error");
//ResponseOrIdle::IdleReject(tag)
let no = Response::build()
.tag(tag)
.message(
"Internal error, processing command triggered an illegal IMAP state transition",
)
.no()
.unwrap();
ResponseOrIdle::IdleReject(no)
}
}
}
pub async fn idle_poll(&mut self) -> ResponseOrIdle {
match self.idle_poll_happy().await {
Ok(r) => r,
Err(e) => {
tracing::error!(err=?e, "something bad happened in idle");
@ -42,7 +77,7 @@ impl Instance {
}
}
pub async fn idle_happy(&mut self) -> Result<ResponseOrIdle> {
pub async fn idle_poll_happy(&mut self) -> Result<ResponseOrIdle> {
let (mbx, tag, stop) = match &mut self.state {
flow::State::Idle(_, ref mut mbx, _, tag, stop) => (mbx, tag.clone(), stop.clone()),
_ => bail!("Invalid session state, can't idle"),
@ -128,10 +163,11 @@ impl Instance {
.bad()
.unwrap());
}
ResponseOrIdle::Response(resp)
match &self.state {
/*match &self.state {
flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()),
_ => ResponseOrIdle::Response(resp),
}
}*/
}
}

View File

@ -16,7 +16,7 @@ use tokio::select;
use tokio::sync::watch;
use tokio_util::compat::*;
use smtp_message::{Email, EscapedDataReader, Reply, ReplyCode};
use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode};
use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata};
use crate::config::*;
@ -181,6 +181,12 @@ impl Config for LmtpServer {
return err_response_stream(meta, format!("io error: {}", e));
}
reader.complete();
let raw_size = text.len();
// Unescape email, shrink it also to remove last dot
let unesc_res = DataUnescaper::new(true).unescape(&mut text);
text.truncate(unesc_res.written);
tracing::debug!(prev_sz = raw_size, new_sz = text.len(), "unescaped");
let encrypted_message = match EncryptedMessage::new(text) {
Ok(x) => Arc::new(x),

View File

@ -21,6 +21,7 @@ pub struct LdapLoginProvider {
storage_specific: StorageSpecific,
in_memory_store: storage::in_memory::MemDb,
garage_store: storage::garage::GarageRoot,
}
enum BucketSource {
@ -60,6 +61,9 @@ impl LdapLoginProvider {
let specific = match config.storage {
LdapStorage::InMemory => StorageSpecific::InMemory,
LdapStorage::Garage(grgconf) => {
attrs_to_retrieve.push(grgconf.aws_access_key_id_attr.clone());
attrs_to_retrieve.push(grgconf.aws_secret_access_key_attr.clone());
let bucket_source =
match (grgconf.default_bucket.clone(), grgconf.bucket_attr.clone()) {
(Some(b), None) => BucketSource::Constant(b),
@ -88,7 +92,11 @@ impl LdapLoginProvider {
mail_attr: config.mail_attr,
crypto_root_attr: config.crypto_root_attr,
storage_specific: specific,
//@FIXME should be created outside of the login provider
//Login provider should return only a cryptoroot + a storage URI
//storage URI that should be resolved outside...
in_memory_store: storage::in_memory::MemDb::new(),
garage_store: storage::garage::GarageRoot::new()?,
})
}
@ -111,7 +119,7 @@ impl LdapLoginProvider {
BucketSource::Attr(a) => get_attr(user, &a)?,
};
storage::garage::GarageBuilder::new(storage::garage::GarageConf {
self.garage_store.user(storage::garage::GarageConf {
region: from_config.aws_region.clone(),
s3_endpoint: from_config.s3_endpoint.clone(),
k2v_endpoint: from_config.k2v_endpoint.clone(),

View File

@ -25,6 +25,7 @@ pub struct UserDatabase {
pub struct StaticLoginProvider {
user_db: watch::Receiver<UserDatabase>,
in_memory_store: storage::in_memory::MemDb,
garage_store: storage::garage::GarageRoot,
}
pub async fn update_user_list(config: PathBuf, up: watch::Sender<UserDatabase>) -> Result<()> {
@ -84,6 +85,7 @@ impl StaticLoginProvider {
Ok(Self {
user_db: rx,
in_memory_store: storage::in_memory::MemDb::new(),
garage_store: storage::garage::GarageRoot::new()?,
})
}
}
@ -109,7 +111,7 @@ impl LoginProvider for StaticLoginProvider {
let storage: storage::Builder = match &user.config.storage {
StaticStorage::InMemory => self.in_memory_store.builder(username).await,
StaticStorage::Garage(grgconf) => {
storage::garage::GarageBuilder::new(storage::garage::GarageConf {
self.garage_store.user(storage::garage::GarageConf {
region: grgconf.aws_region.clone(),
k2v_endpoint: grgconf.k2v_endpoint.clone(),
s3_endpoint: grgconf.s3_endpoint.clone(),
@ -140,7 +142,7 @@ impl LoginProvider for StaticLoginProvider {
let storage: storage::Builder = match &user.config.storage {
StaticStorage::InMemory => self.in_memory_store.builder(&user.username).await,
StaticStorage::Garage(grgconf) => {
storage::garage::GarageBuilder::new(storage::garage::GarageConf {
self.garage_store.user(storage::garage::GarageConf {
region: grgconf.aws_region.clone(),
k2v_endpoint: grgconf.k2v_endpoint.clone(),
s3_endpoint: grgconf.s3_endpoint.clone(),

View File

@ -498,7 +498,7 @@ fn dump(uid_index: &Bayou<UidIndex>) {
/// The metadata of a message that is stored in K2V
/// at pk = mail/<mailbox uuid>, sk = <message uuid>
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MailMeta {
/// INTERNALDATE field (milliseconds since epoch)
pub internaldate: u64,

View File

@ -2,7 +2,8 @@ use super::mailbox::MailMeta;
use super::snapshot::FrozenMailbox;
use super::unique_ident::UniqueIdent;
use anyhow::Result;
use futures::stream::{FuturesOrdered, StreamExt};
use futures::future::FutureExt;
use futures::stream::{BoxStream, Stream, StreamExt};
/// Query is in charge of fetching efficiently
/// requested data for a list of emails
@ -28,64 +29,62 @@ impl QueryScope {
}
}
//type QueryResultStream = Box<dyn Stream<Item = Result<QueryResult>>>;
impl<'a, 'b> Query<'a, 'b> {
pub async fn fetch(&self) -> Result<Vec<QueryResult>> {
pub fn fetch(&self) -> BoxStream<Result<QueryResult>> {
match self.scope {
QueryScope::Index => Ok(self
.emails
.iter()
.map(|&uuid| QueryResult::IndexResult { uuid })
.collect()),
QueryScope::Partial => self.partial().await,
QueryScope::Full => self.full().await,
QueryScope::Index => Box::pin(
futures::stream::iter(self.emails)
.map(|&uuid| Ok(QueryResult::IndexResult { uuid })),
),
QueryScope::Partial => Box::pin(self.partial()),
QueryScope::Full => Box::pin(self.full()),
}
}
// --- functions below are private *for reasons*
fn partial<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send {
async move {
let maybe_meta_list: Result<Vec<MailMeta>> =
self.frozen.mailbox.fetch_meta(self.emails).await;
let list_res = maybe_meta_list
.map(|meta_list| {
meta_list
.into_iter()
.zip(self.emails)
.map(|(metadata, &uuid)| Ok(QueryResult::PartialResult { uuid, metadata }))
.collect()
})
.unwrap_or_else(|e| vec![Err(e)]);
async fn partial(&self) -> Result<Vec<QueryResult>> {
let meta = self.frozen.mailbox.fetch_meta(self.emails).await?;
let result = meta
.into_iter()
.zip(self.emails.iter())
.map(|(metadata, &uuid)| QueryResult::PartialResult { uuid, metadata })
.collect::<Vec<_>>();
Ok(result)
futures::stream::iter(list_res)
}
.flatten_stream()
}
/// @FIXME WARNING: THIS CAN ALLOCATE A LOT OF MEMORY
/// AND GENERATE SO MUCH NETWORK TRAFFIC.
/// THIS FUNCTION SHOULD BE REWRITTEN, FOR EXAMPLE WITH
/// SOMETHING LIKE AN ITERATOR
async fn full(&self) -> Result<Vec<QueryResult>> {
let meta_list = self.partial().await?;
meta_list
.into_iter()
.map(|meta| async move {
let content = self
.frozen
.mailbox
.fetch_full(
*meta.uuid(),
&meta
.metadata()
.expect("meta to be PartialResult")
.message_key,
)
.await?;
fn full<'d>(&'d self) -> impl Stream<Item = Result<QueryResult>> + 'd + Send {
self.partial().then(move |maybe_meta| async move {
let meta = maybe_meta?;
Ok(meta.into_full(content).expect("meta to be PartialResult"))
})
.collect::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
let content = self
.frozen
.mailbox
.fetch_full(
*meta.uuid(),
&meta
.metadata()
.expect("meta to be PartialResult")
.message_key,
)
.await?;
Ok(meta.into_full(content).expect("meta to be PartialResult"))
})
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum QueryResult {
IndexResult {
uuid: UniqueIdent,

View File

@ -34,7 +34,12 @@ struct Args {
#[clap(long)]
dev: bool,
#[clap(short, long, env = "AEROGRAMME_CONFIG", default_value = "aerogramme.toml")]
#[clap(
short,
long,
env = "AEROGRAMME_CONFIG",
default_value = "aerogramme.toml"
)]
/// Path to the main Aerogramme configuration file
config_file: PathBuf,
}
@ -187,7 +192,10 @@ async fn main() -> Result<()> {
hostname: "example.tld".to_string(),
}),
auth: Some(AuthConfig {
bind_addr: SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 12345),
bind_addr: SocketAddr::new(
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)),
12345,
),
}),
users: UserManagement::Demo,
})

View File

@ -7,9 +7,9 @@ use futures::try_join;
use log::*;
use tokio::sync::watch;
use crate::auth;
use crate::config::*;
use crate::imap;
use crate::auth;
use crate::lmtp::*;
use crate::login::ArcLoginProvider;
use crate::login::{demo_provider::*, ldap_provider::*, static_provider::*};
@ -47,9 +47,16 @@ impl Server {
};
let lmtp_server = config.lmtp.map(|lmtp| LmtpServer::new(lmtp, login.clone()));
let imap_unsecure_server = config.imap_unsecure.map(|imap| imap::new_unsecure(imap, login.clone()));
let imap_server = config.imap.map(|imap| imap::new(imap, login.clone())).transpose()?;
let auth_server = config.auth.map(|auth| auth::AuthServer::new(auth, login.clone()));
let imap_unsecure_server = config
.imap_unsecure
.map(|imap| imap::new_unsecure(imap, login.clone()));
let imap_server = config
.imap
.map(|imap| imap::new(imap, login.clone()))
.transpose()?;
let auth_server = config
.auth
.map(|auth| auth::AuthServer::new(auth, login.clone()));
Ok(Self {
lmtp_server,

View File

@ -1,7 +1,45 @@
use crate::storage::*;
use aws_sdk_s3::{self as s3, error::SdkError, operation::get_object::GetObjectError};
use aws_smithy_runtime::client::http::hyper_014::HyperClientBuilder;
use aws_smithy_runtime_api::client::http::SharedHttpClient;
use hyper_rustls::HttpsConnector;
use hyper_util::client::legacy::{connect::HttpConnector, Client as HttpClient};
use hyper_util::rt::TokioExecutor;
use serde::Serialize;
use crate::storage::*;
pub struct GarageRoot {
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
aws_http: SharedHttpClient,
}
impl GarageRoot {
pub fn new() -> anyhow::Result<Self> {
let connector = hyper_rustls::HttpsConnectorBuilder::new()
.with_native_roots()?
.https_or_http()
.enable_http1()
.enable_http2()
.build();
let k2v_http = HttpClient::builder(TokioExecutor::new()).build(connector);
let aws_http = HyperClientBuilder::new().build_https();
Ok(Self { k2v_http, aws_http })
}
pub fn user(&self, conf: GarageConf) -> anyhow::Result<Arc<GarageUser>> {
let mut unicity: Vec<u8> = vec![];
unicity.extend_from_slice(file!().as_bytes());
unicity.append(&mut rmp_serde::to_vec(&conf)?);
Ok(Arc::new(GarageUser {
conf,
aws_http: self.aws_http.clone(),
k2v_http: self.k2v_http.clone(),
unicity,
}))
}
}
#[derive(Clone, Debug, Serialize)]
pub struct GarageConf {
pub region: String,
@ -12,23 +50,19 @@ pub struct GarageConf {
pub bucket: String,
}
//@FIXME we should get rid of this builder
//and allocate a S3 + K2V client only once per user
//(and using a shared HTTP client)
#[derive(Clone, Debug)]
pub struct GarageBuilder {
pub struct GarageUser {
conf: GarageConf,
aws_http: SharedHttpClient,
k2v_http: HttpClient<HttpsConnector<HttpConnector>, k2v_client::Body>,
unicity: Vec<u8>,
}
impl GarageBuilder {
pub fn new(conf: GarageConf) -> anyhow::Result<Arc<Self>> {
let mut unicity: Vec<u8> = vec![];
unicity.extend_from_slice(file!().as_bytes());
unicity.append(&mut rmp_serde::to_vec(&conf)?);
Ok(Arc::new(Self { conf, unicity }))
}
}
#[async_trait]
impl IBuilder for GarageBuilder {
impl IBuilder for GarageUser {
async fn build(&self) -> Result<Store, StorageError> {
let s3_creds = s3::config::Credentials::new(
self.conf.aws_access_key_id.clone(),
@ -41,6 +75,7 @@ impl IBuilder for GarageBuilder {
let sdk_config = aws_config::from_env()
.region(aws_config::Region::new(self.conf.region.clone()))
.credentials_provider(s3_creds)
.http_client(self.aws_http.clone())
.endpoint_url(self.conf.s3_endpoint.clone())
.load()
.await;
@ -60,13 +95,14 @@ impl IBuilder for GarageBuilder {
user_agent: None,
};
let k2v_client = match k2v_client::K2vClient::new(k2v_config) {
Err(e) => {
tracing::error!("unable to build k2v client: {}", e);
return Err(StorageError::Internal);
}
Ok(v) => v,
};
let k2v_client =
match k2v_client::K2vClient::new_with_client(k2v_config, self.k2v_http.clone()) {
Err(e) => {
tracing::error!("unable to build k2v client: {}", e);
return Err(StorageError::Internal);
}
Ok(v) => v,
};
Ok(Box::new(GarageStore {
bucket: self.conf.bucket.clone(),
@ -105,6 +141,7 @@ fn causal_to_row_val(row_ref: RowRef, causal_value: k2v_client::CausalValue) ->
#[async_trait]
impl IStore for GarageStore {
async fn row_fetch<'a>(&self, select: &Selector<'a>) -> Result<Vec<RowVal>, StorageError> {
tracing::trace!(select=%select, command="row_fetch");
let (pk_list, batch_op) = match select {
Selector::Range {
shard,
@ -196,21 +233,26 @@ impl IStore for GarageStore {
}
Ok(v) => v,
};
//println!("fetch res -> {:?}", all_raw_res);
let row_vals = all_raw_res
.into_iter()
.fold(vec![], |mut acc, v| {
acc.extend(v.items);
acc
})
.into_iter()
.zip(pk_list.into_iter())
.map(|((sk, cv), pk)| causal_to_row_val(RowRef::new(&pk, &sk), cv))
.collect::<Vec<_>>();
let row_vals =
all_raw_res
.into_iter()
.zip(pk_list.into_iter())
.fold(vec![], |mut acc, (page, pk)| {
page.items
.into_iter()
.map(|(sk, cv)| causal_to_row_val(RowRef::new(&pk, &sk), cv))
.for_each(|rr| acc.push(rr));
acc
});
tracing::debug!(fetch_count = row_vals.len(), command = "row_fetch");
Ok(row_vals)
}
async fn row_rm<'a>(&self, select: &Selector<'a>) -> Result<(), StorageError> {
tracing::trace!(select=%select, command="row_rm");
let del_op = match select {
Selector::Range {
shard,
@ -280,6 +322,7 @@ impl IStore for GarageStore {
}
async fn row_insert(&self, values: Vec<RowVal>) -> Result<(), StorageError> {
tracing::trace!(entries=%values.iter().map(|v| v.row_ref.to_string()).collect::<Vec<_>>().join(","), command="row_insert");
let batch_ops = values
.iter()
.map(|v| k2v_client::BatchInsertOp {
@ -307,6 +350,7 @@ impl IStore for GarageStore {
}
}
async fn row_poll(&self, value: &RowRef) -> Result<RowVal, StorageError> {
tracing::trace!(entry=%value, command="row_poll");
loop {
if let Some(ct) = &value.causality {
match self
@ -343,6 +387,7 @@ impl IStore for GarageStore {
}
async fn blob_fetch(&self, blob_ref: &BlobRef) -> Result<BlobVal, StorageError> {
tracing::trace!(entry=%blob_ref, command="blob_fetch");
let maybe_out = self
.s3
.get_object()
@ -382,6 +427,7 @@ impl IStore for GarageStore {
Ok(bv)
}
async fn blob_insert(&self, blob_val: BlobVal) -> Result<(), StorageError> {
tracing::trace!(entry=%blob_val.blob_ref, command="blob_insert");
let streamable_value = s3::primitives::ByteStream::from(blob_val.value);
let maybe_send = self
@ -406,6 +452,7 @@ impl IStore for GarageStore {
}
}
async fn blob_copy(&self, src: &BlobRef, dst: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(src=%src, dst=%dst, command="blob_copy");
let maybe_copy = self
.s3
.copy_object()
@ -433,6 +480,7 @@ impl IStore for GarageStore {
}
}
async fn blob_list(&self, prefix: &str) -> Result<Vec<BlobRef>, StorageError> {
tracing::trace!(prefix = prefix, command = "blob_list");
let maybe_list = self
.s3
.list_objects_v2()
@ -462,6 +510,7 @@ impl IStore for GarageStore {
}
}
async fn blob_rm(&self, blob_ref: &BlobRef) -> Result<(), StorageError> {
tracing::trace!(entry=%blob_ref, command="blob_rm");
let maybe_delete = self
.s3
.delete_object()

View File

@ -225,7 +225,7 @@ fn rfc4551_imapext_condstore() {
FetchKind::Rfc822Size,
FetchMod::ChangedSince(2),
)?;
assert!(fetch_res.contains("* 1 FETCH (RFC822.SIZE 84 MODSEQ (3))"));
assert!(fetch_res.contains("* 1 FETCH (RFC822.SIZE 81 MODSEQ (3))"));
assert!(!fetch_res.contains("* 2 FETCH"));
assert_eq!(store_res.lines().count(), 2);

View File

@ -384,8 +384,7 @@ pub fn append(imap: &mut TcpStream, content: Email) -> Result<String> {
// write our stuff
imap.write(ref_mail)?;
imap.write(&b"\r\n"[..])?;
let read = read_lines(imap, &mut buffer, None)?;
assert_eq!(&read[..5], &b"47 OK"[..]);
let read = read_lines(imap, &mut buffer, Some(&b"47 OK"[..]))?;
let srv_msg = std::str::from_utf8(read)?;
Ok(srv_msg.to_string())

1
tests/emails/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.zstd filter=lfs diff=lfs merge=lfs -text

1
tests/emails/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.mbox

BIN
tests/emails/aero100.mbox.zstd (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -0,0 +1,240 @@
Append { mailbox: Other(MailboxOther(Atom(AtomExt("Sent")))), flags: [Seen], date: None, message: REDACTED } }
Append { mailbox: Other(MailboxOther(String(Quoted(Quoted("Drafts"))))), flags: [Seen, Draft], date: None, message: REDACTED } }
Append { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), flags: [Seen], date: None, message: REDACTED } }
Append { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), flags: [Seen], date: Some(2024-02-14T14:12:35+01:00), message: REDACTED } }
Capability
Check
Close
Create { mailbox: Other(MailboxOther(Atom(AtomExt("Mailspring")))) } }
Create { mailbox: Other(MailboxOther(String(Quoted(Quoted("Dataset"))))) } }
Create { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring.Snoozed"))))) } }
Enable { capabilities: [CondStore]+ } }
Enable { capabilities: [Utf8(Accept)]+ } }
Examine { mailbox: Inbox, modifiers: [] } }
Expunge { uid_sequence_set: None } }
Fetch { sequence_set: SequenceSet([Range(Asterisk, Value(5))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Envelope, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(12), Value(13))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Rfc822Size, Flags, ModSeq, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(22)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(24)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(5)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(638435220681800000)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(638435229950250000)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(638435249520030000)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(6)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(7)], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [ChangedSince(8)], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, InternalDate, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SENDER")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("TO")), Atom(AtomExt("CC")), Atom(AtomExt("MESSAGE-ID")), Atom(AtomExt("REFERENCES")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("CONTENT-DESCRIPTION")), Atom(AtomExt("IN-REPLY-TO")), Atom(AtomExt("REPLY-TO")), Atom(AtomExt("LINES")), Atom(AtomExt("LIST-POST")), Atom(AtomExt("X-LABEL"))]+)), partial: None, peek: true }]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(20))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(20))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(23))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags, Uid]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Flags, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("X-MS-TNEF-Correlator")), Atom(AtomExt("CONTENT-CLASS")), Atom(AtomExt("IMPORTANCE")), Atom(AtomExt("PRIORITY")), Atom(AtomExt("X-PRIORITY")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }, BodyStructure]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, InternalDate, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SENDER")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("TO")), Atom(AtomExt("CC")), Atom(AtomExt("MESSAGE-ID")), Atom(AtomExt("REFERENCES")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("CONTENT-DESCRIPTION")), Atom(AtomExt("IN-REPLY-TO")), Atom(AtomExt("REPLY-TO")), Atom(AtomExt("LINES")), Atom(AtomExt("LIST-POST")), Atom(AtomExt("X-LABEL"))]+)), partial: None, peek: true }]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26))]+), modifiers: [ChangedSince(68)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26))]+), modifiers: [ChangedSince(69)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34))]+), modifiers: [ChangedSince(102)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34))]+), modifiers: [ChangedSince(189)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34))]+), modifiers: [ChangedSince(81)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34)), Range(Value(60), Value(62))]+), modifiers: [ChangedSince(165)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34)), Range(Value(60), Value(62))]+), modifiers: [ChangedSince(166)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(2)), Range(Value(11), Value(13)), Range(Value(18), Value(19)), Range(Value(22), Value(26)), Range(Value(33), Value(34)), Range(Value(60), Value(62))]+), modifiers: [ChangedSince(168)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags, Uid]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Flags, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("X-MS-TNEF-Correlator")), Atom(AtomExt("CONTENT-CLASS")), Atom(AtomExt("IMPORTANCE")), Atom(AtomExt("PRIORITY")), Atom(AtomExt("X-PRIORITY")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }, BodyStructure]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(6))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(7))]+), modifiers: [ChangedSince(9)], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(1), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(20), Value(23))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(27), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(2), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(30), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(3), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(3), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(4), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(5), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(5), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Flags, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("X-MS-TNEF-Correlator")), Atom(AtomExt("CONTENT-CLASS")), Atom(AtomExt("IMPORTANCE")), Atom(AtomExt("PRIORITY")), Atom(AtomExt("X-PRIORITY")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }, BodyStructure]), uid: false } }
Fetch { sequence_set: SequenceSet([Range(Value(5), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(60), Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Rfc822Size, Flags, ModSeq, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(60), Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(63), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(6), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(6), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, InternalDate, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(6), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, InternalDate, Envelope, BodyStructure, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("REFERENCES")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("FROM")), Atom(AtomExt("SENDER")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(6), Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(86), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(8), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Range(Value(9), Asterisk)]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(13))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Header(None)), partial: None, peek: true }, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Flags, Uid]), uid: false } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([1]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: false }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Resent-Message-ID")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To")), Atom(AtomExt("List-Unsubscribe")), Atom(AtomExt("Received")), Atom(AtomExt("Delivery-Date"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1)), Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("bcc")), Atom(AtomExt("cc")), Atom(AtomExt("date")), Atom(AtomExt("from")), Atom(AtomExt("reply-to")), Atom(AtomExt("sender")), Atom(AtomExt("subject")), Atom(AtomExt("to"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1)), Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("in-reply-to")), Atom(AtomExt("message-id")), Atom(AtomExt("references"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1)), Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1)), Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Rfc822Size, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(1)), Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(22))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(24))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(24))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, InternalDate, Envelope, BodyStructure, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("REFERENCES")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("FROM")), Atom(AtomExt("SENDER")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(24))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(24))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(24)), Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Resent-Message-ID")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To")), Atom(AtomExt("List-Unsubscribe")), Atom(AtomExt("Received")), Atom(AtomExt("Delivery-Date"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(29))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(29))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: false } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Envelope, InternalDate, Rfc822Size, Flags, BodyStructure, Uid, BodyExt { section: Some(Header(None)), partial: None, peek: true }, Rfc822Size, InternalDate]), uid: false } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([1]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(2))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(2)), Single(Value(8))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Flags, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("DATE")), Atom(AtomExt("FROM")), Atom(AtomExt("SUBJECT")), Atom(AtomExt("CONTENT-TYPE")), Atom(AtomExt("X-MS-TNEF-Correlator")), Atom(AtomExt("CONTENT-CLASS")), Atom(AtomExt("IMPORTANCE")), Atom(AtomExt("PRIORITY")), Atom(AtomExt("X-PRIORITY")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("REPLY-TO"))]+)), partial: None, peek: true }, BodyStructure]), uid: false } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([1]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([2]+))), partial: Some((0, 10240)), peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, BodyStructure, InternalDate, Rfc822Size]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, InternalDate, Envelope, BodyStructure, Rfc822Size, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("REFERENCES")), Atom(AtomExt("THREAD-TOPIC")), Atom(AtomExt("FROM")), Atom(AtomExt("SENDER")), Atom(AtomExt("REPLY-TO")), Atom(AtomExt("AUTO-SUBMITTED")), Atom(AtomExt("BOUNCES-TO")), Atom(AtomExt("LIST-ARCHIVE")), Atom(AtomExt("LIST-HELP")), Atom(AtomExt("LIST-ID")), Atom(AtomExt("LIST-OWNER")), Atom(AtomExt("LIST-POST")), Atom(AtomExt("LIST-SUBSCRIBE")), Atom(AtomExt("LIST-UNSUBSCRIBE")), Atom(AtomExt("PRECEDENCE")), Atom(AtomExt("RESENT-FROM")), Atom(AtomExt("RETURN-PATH"))]+)), partial: None, peek: true }, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(3)), Single(Value(2)), Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Resent-Message-ID")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To")), Atom(AtomExt("List-Unsubscribe")), Atom(AtomExt("Received")), Atom(AtomExt("Delivery-Date"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([2, 1]+))), partial: None, peek: true }, BodyExt { section: Some(Part(Part([2, 2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(4))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(4)), Single(Value(3)), Single(Value(2)), Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(5))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(5))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([1, 2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(5))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(5))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(60))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Part(Part([1, 2]+))), partial: Some((0, 2227)), peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(60))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(60))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyStructure, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(60))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(61))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Header(None)), partial: None, peek: true }, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Part(Part([1, 2]+))), partial: Some((0, 711)), peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyStructure, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(62))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(6))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(6))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(6))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Flags, Envelope, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("References"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(6))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Header(None)), partial: None, peek: true }, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Rfc822Size, Flags, ModSeq, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Rfc822Header, BodyExt { section: Some(Part(Part([1, 2]+))), partial: None, peek: true }, BodyExt { section: Some(Part(Part([2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, BodyExt { section: Some(Part(Part([1, 2]+))), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, BodyExt { section: None, partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(7)), Single(Value(6)), Single(Value(5)), Single(Value(4)), Single(Value(3)), Single(Value(2)), Single(Value(1))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([Uid, Rfc822Size, Flags, BodyExt { section: Some(HeaderFields(None, [Atom(AtomExt("From")), Atom(AtomExt("To")), Atom(AtomExt("Cc")), Atom(AtomExt("Bcc")), Atom(AtomExt("Resent-Message-ID")), Atom(AtomExt("Subject")), Atom(AtomExt("Date")), Atom(AtomExt("Message-ID")), Atom(AtomExt("Priority")), Atom(AtomExt("X-Priority")), Atom(AtomExt("References")), Atom(AtomExt("Newsgroups")), Atom(AtomExt("In-Reply-To")), Atom(AtomExt("Content-Type")), Atom(AtomExt("Reply-To")), Atom(AtomExt("List-Unsubscribe")), Atom(AtomExt("Received")), Atom(AtomExt("Delivery-Date"))]+)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(83))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([BodyExt { section: Some(Header(None)), partial: None, peek: true }, BodyExt { section: Some(Text(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(83))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Rfc822Size, Flags, ModSeq, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Fetch { sequence_set: SequenceSet([Single(Value(85))]+), modifiers: [], macro_or_item_names: MessageDataItemNames([InternalDate, Uid, Rfc822Size, Flags, ModSeq, BodyExt { section: Some(Header(None)), partial: None, peek: true }]), uid: true } }
Idle
List { reference: Other(MailboxOther(String(Quoted(Quoted("Archive."))))), mailbox_wildcard: Token(ListCharString("*")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted("INBOX."))))), mailbox_wildcard: Token(ListCharString("*")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted("Dataset"))), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted("INBOX"))), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted(""))), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted("*"))), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted("*"))), return: [Status([Unseen])] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: Token(ListCharString("INBOX")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: Token(ListCharString("%")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted("Mailspring."))))), mailbox_wildcard: Token(ListCharString("*")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted("Sent."))))), mailbox_wildcard: Token(ListCharString("*")), return: [] } }
List { reference: Other(MailboxOther(String(Quoted(Quoted("Trash."))))), mailbox_wildcard: Token(ListCharString("*")), return: [] } }
Login { username: Atom(AtomExt("REDACTED")), password: /* REDACTED */ } }
Login { username: String(Quoted(Quoted("REDACTED"))), password: /* REDACTED */ } }
Login { username: String(Quoted(Quoted("REDACTED@saint-ex.deuxfleurs.org"))), password: /* REDACTED */ } }
Logout
Lsub { reference: Other(MailboxOther(String(Quoted(Quoted(""))))), mailbox_wildcard: String(Quoted(Quoted("*"))) } }
Move { sequence_set: SequenceSet([Range(Value(20), Value(21))]+), mailbox: Other(MailboxOther(String(Quoted(Quoted("Dataset"))))), uid: true } }
Move { sequence_set: SequenceSet([Single(Value(29))]+), mailbox: Other(MailboxOther(String(Quoted(Quoted("Dataset"))))), uid: true } }
Noop
Search { charset: None, criteria: And([Header(Atom(AtomExt("Message-ID")), Atom(AtomExt("<Mailbird-8d3b03f5-7737-479e-b8d7-a3cae013e72f@saint-ex.deuxfleurs.org>"))), Undeleted]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Asterisk)]+)), Deleted]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Asterisk)]+)), Not(Deleted)]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Asterisk)]+)), Since(2023-11-16)]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Asterisk)]+)), Unseen]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Value(1))]+)), Not(Deleted)]+), uid: true } }
Search { charset: None, criteria: And([SequenceSet(SequenceSet([Range(Value(1), Value(4))]+)), Not(Deleted)]+), uid: true } }
Search { charset: None, criteria: And([Since(2024-02-07), All]+), uid: false } }
Search { charset: None, criteria: And([Since(2024-02-08), All]+), uid: false } }
Search { charset: None, criteria: And([Since(2024-02-09), All]+), uid: false } }
Search { charset: None, criteria: And([Undeleted, Since(2023-11-17)]+), uid: false } }
Search { charset: None, criteria: And([Undeleted, Since(2024-02-13)]+), uid: false } }
Search { charset: None, criteria: Before(2024-02-09), uid: true } }
Search { charset: None, criteria: Since(2024-01-31), uid: true } }
Select { mailbox: Inbox, modifiers: [] } }
Select { mailbox: Inbox, modifiers: [Condstore] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Archive")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Drafts")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Mailspring")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Sent")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Test.Coucou")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Test")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(Atom(AtomExt("Trash")))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Archive"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Dataset"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Drafts"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("INBOX.Pourriel"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring.Snoozed"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), modifiers: [] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), modifiers: [Condstore] } }
Select { mailbox: Other(MailboxOther(String(Quoted(Quoted("Trash"))))), modifiers: [] } }
Status { mailbox: Inbox, item_names: [Messages, Recent, UidNext, UidValidity, Unseen] } }
Status { mailbox: Inbox, item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(Atom(AtomExt("Archive")))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(Atom(AtomExt("Drafts")))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(Atom(AtomExt("Mailspring")))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(Atom(AtomExt("Sent")))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(Atom(AtomExt("Trash")))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Archive"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Archive"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Drafts"))))), item_names: [Messages] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Drafts"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Drafts"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("INBOX.Pourriel"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("INBOX.Pourriel"))))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring.Snoozed"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring.Snoozed"))))), item_names: [Unseen, Messages, Recent, UidNext, UidValidity, HighestModSeq] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), item_names: [UidNext, Messages, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Sent"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Test.Coucou"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Test"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Status { mailbox: Other(MailboxOther(String(Quoted(Quoted("Trash"))))), item_names: [UidNext, UidValidity, Unseen, Recent] } }
Store { sequence_set: SequenceSet([Range(Value(60), Value(62))]+), kind: Add, response: Answer, flags: [Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Range(Value(60), Value(62))]+), kind: Add, response: Answer, flags: [Deleted, Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Range(Value(60), Value(62))]+), kind: Add, response: Answer, flags: [Seen, Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(1))]+), kind: Add, response: Answer, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(24))]+), kind: Add, response: Answer, flags: [Seen, Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(24))]+), kind: Add, response: Answer, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(26))]+), kind: Add, response: Answer, flags: [Answered], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(29))]+), kind: Add, response: Answer, flags: [Keyword(Atom("NonJunk"))], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(29))]+), kind: Add, response: Answer, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(2))]+), kind: Add, response: Answer, flags: [Seen], modifiers: [], uid: false } }
Store { sequence_set: SequenceSet([Single(Value(2))]+), kind: Add, response: Silent, flags: [Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(3))]+), kind: Add, response: Answer, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(3))]+), kind: Add, response: Silent, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(5))]+), kind: Add, response: Answer, flags: [Keyword(Atom("NonJunk"))], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(60))]+), kind: Add, response: Answer, flags: [Seen, Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(60))]+), kind: Add, response: Silent, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(61))]+), kind: Add, response: Answer, flags: [Seen, Deleted], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(61))]+), kind: Add, response: Silent, flags: [Seen], modifiers: [], uid: true } }
Store { sequence_set: SequenceSet([Single(Value(62))]+), kind: Add, response: Silent, flags: [Seen], modifiers: [], uid: true } }
Subscribe { mailbox: Other(MailboxOther(Atom(AtomExt("Mailspring")))) } }
Subscribe { mailbox: Other(MailboxOther(String(Quoted(Quoted("Dataset"))))) } }
Subscribe { mailbox: Other(MailboxOther(String(Quoted(Quoted("Mailspring.Snoozed"))))) } }
Unselect

View File

@ -0,0 +1,45 @@
count,command,aggregation
6,Append,Raw
33,Capability,Raw
96,Check,Raw
6,Close,Raw
3,Create,Raw
9,Enable,Raw
26,Examine,Raw
1,Expunge,Raw
1187,Fetch,Raw
248,Idle,Raw
132,List,Raw
244,Login,Raw
169,Logout,Raw
14,Lsub,Raw
2,Move,Raw
295,Noop,Raw
658,Search,Raw
746,Select,Raw
203,Status,Raw
23,Store,Raw
3,Subscribe,Raw
515,Unselect,Raw
6,Append,Unique
1,Capability,Unique
1,Check,Unique
1,Close,Unique
3,Create,Unique
2,Enable,Unique
1,Examine,Unique
1,Expunge,Unique
128,Fetch,Unique
1,Idle,Unique
12,List,Unique
9,Login,Unique
1,Logout,Unique
1,Lsub,Unique
2,Move,Unique
1,Noop,Unique
14,Search,Unique
18,Select,Unique
22,Status,Unique
19,Store,Unique
3,Subscribe,Unique
1,Unselect,Unique
1 count command aggregation
2 6 Append Raw
3 33 Capability Raw
4 96 Check Raw
5 6 Close Raw
6 3 Create Raw
7 9 Enable Raw
8 26 Examine Raw
9 1 Expunge Raw
10 1187 Fetch Raw
11 248 Idle Raw
12 132 List Raw
13 244 Login Raw
14 169 Logout Raw
15 14 Lsub Raw
16 2 Move Raw
17 295 Noop Raw
18 658 Search Raw
19 746 Select Raw
20 203 Status Raw
21 23 Store Raw
22 3 Subscribe Raw
23 515 Unselect Raw
24 6 Append Unique
25 1 Capability Unique
26 1 Check Unique
27 1 Close Unique
28 3 Create Unique
29 2 Enable Unique
30 1 Examine Unique
31 1 Expunge Unique
32 128 Fetch Unique
33 1 Idle Unique
34 12 List Unique
35 9 Login Unique
36 1 Logout Unique
37 1 Lsub Unique
38 2 Move Unique
39 1 Noop Unique
40 14 Search Unique
41 18 Select Unique
42 22 Status Unique
43 19 Store Unique
44 3 Subscribe Unique
45 1 Unselect Unique

File diff suppressed because it is too large Load Diff

14
tests/emails/report.R Normal file
View File

@ -0,0 +1,14 @@
library(tidyverse)
library(lubridate)
read_csv("imap_commands_summary.csv") -> cmd
ggplot(cmd, aes(x=command, y=count)) +
geom_bar(stat = "identity")+
theme_classic() +
facet_wrap(~aggregation, ncol=1, scales = "free")
read_csv("mailbox_email_sizes.csv") -> mbx
ggplot(mbx, aes(x=size, colour=mailbox)) +
stat_ecdf(pad=FALSE,geom = "step") +
scale_x_log10()+ theme_classic()