From 11462f80c4ae25696c7436ed7aacb92074d7e911 Mon Sep 17 00:00:00 2001 From: Quentin Dufour Date: Fri, 8 Mar 2024 09:55:33 +0100 Subject: Re-enable proto --- Cargo.lock | 927 +++++++++++++++++++++++++-- Cargo.toml | 4 +- aero-dav/src/caldecoder.rs | 10 +- aero-dav/src/calencoder.rs | 8 +- aero-dav/src/decoder.rs | 15 +- aero-dav/src/lib.rs | 1 - aero-dav/src/realization.rs | 4 +- aero-proto/Cargo.toml | 35 + aero-proto/dav.rs | 145 ----- aero-proto/imap/attributes.rs | 77 --- aero-proto/imap/capability.rs | 159 ----- aero-proto/imap/command/anonymous.rs | 83 --- aero-proto/imap/command/anystate.rs | 54 -- aero-proto/imap/command/authenticated.rs | 683 -------------------- aero-proto/imap/command/mod.rs | 20 - aero-proto/imap/command/selected.rs | 424 ------------ aero-proto/imap/flags.rs | 30 - aero-proto/imap/flow.rs | 114 ---- aero-proto/imap/imf_view.rs | 109 ---- aero-proto/imap/index.rs | 211 ------ aero-proto/imap/mail_view.rs | 306 --------- aero-proto/imap/mailbox_view.rs | 772 ---------------------- aero-proto/imap/mime_view.rs | 580 ----------------- aero-proto/imap/mod.rs | 421 ------------ aero-proto/imap/request.rs | 9 - aero-proto/imap/response.rs | 124 ---- aero-proto/imap/search.rs | 477 -------------- aero-proto/imap/session.rs | 173 ----- aero-proto/lmtp.rs | 221 ------- aero-proto/sasl.rs | 140 ---- aero-proto/src/dav.rs | 146 +++++ aero-proto/src/imap/attributes.rs | 77 +++ aero-proto/src/imap/capability.rs | 159 +++++ aero-proto/src/imap/command/anonymous.rs | 84 +++ aero-proto/src/imap/command/anystate.rs | 54 ++ aero-proto/src/imap/command/authenticated.rs | 682 ++++++++++++++++++++ aero-proto/src/imap/command/mod.rs | 20 + aero-proto/src/imap/command/selected.rs | 425 ++++++++++++ aero-proto/src/imap/flags.rs | 30 + aero-proto/src/imap/flow.rs | 115 ++++ aero-proto/src/imap/imf_view.rs | 109 ++++ aero-proto/src/imap/index.rs | 211 ++++++ aero-proto/src/imap/mail_view.rs | 306 +++++++++ aero-proto/src/imap/mailbox_view.rs | 772 ++++++++++++++++++++++ aero-proto/src/imap/mime_view.rs | 582 +++++++++++++++++ aero-proto/src/imap/mod.rs | 417 ++++++++++++ aero-proto/src/imap/request.rs | 9 + aero-proto/src/imap/response.rs | 124 ++++ aero-proto/src/imap/search.rs | 478 ++++++++++++++ aero-proto/src/imap/session.rs | 175 +++++ aero-proto/src/lib.rs | 6 + aero-proto/src/lmtp.rs | 219 +++++++ aero-proto/src/sasl.rs | 142 ++++ aero-sasl/src/flow.rs | 8 +- 54 files changed, 6273 insertions(+), 5413 deletions(-) create mode 100644 aero-proto/Cargo.toml delete mode 100644 aero-proto/dav.rs delete mode 100644 aero-proto/imap/attributes.rs delete mode 100644 aero-proto/imap/capability.rs delete mode 100644 aero-proto/imap/command/anonymous.rs delete mode 100644 aero-proto/imap/command/anystate.rs delete mode 100644 aero-proto/imap/command/authenticated.rs delete mode 100644 aero-proto/imap/command/mod.rs delete mode 100644 aero-proto/imap/command/selected.rs delete mode 100644 aero-proto/imap/flags.rs delete mode 100644 aero-proto/imap/flow.rs delete mode 100644 aero-proto/imap/imf_view.rs delete mode 100644 aero-proto/imap/index.rs delete mode 100644 aero-proto/imap/mail_view.rs delete mode 100644 aero-proto/imap/mailbox_view.rs delete mode 100644 aero-proto/imap/mime_view.rs delete mode 100644 aero-proto/imap/mod.rs delete mode 100644 aero-proto/imap/request.rs delete mode 100644 aero-proto/imap/response.rs delete mode 100644 aero-proto/imap/search.rs delete mode 100644 aero-proto/imap/session.rs delete mode 100644 aero-proto/lmtp.rs delete mode 100644 aero-proto/sasl.rs create mode 100644 aero-proto/src/dav.rs create mode 100644 aero-proto/src/imap/attributes.rs create mode 100644 aero-proto/src/imap/capability.rs create mode 100644 aero-proto/src/imap/command/anonymous.rs create mode 100644 aero-proto/src/imap/command/anystate.rs create mode 100644 aero-proto/src/imap/command/authenticated.rs create mode 100644 aero-proto/src/imap/command/mod.rs create mode 100644 aero-proto/src/imap/command/selected.rs create mode 100644 aero-proto/src/imap/flags.rs create mode 100644 aero-proto/src/imap/flow.rs create mode 100644 aero-proto/src/imap/imf_view.rs create mode 100644 aero-proto/src/imap/index.rs create mode 100644 aero-proto/src/imap/mail_view.rs create mode 100644 aero-proto/src/imap/mailbox_view.rs create mode 100644 aero-proto/src/imap/mime_view.rs create mode 100644 aero-proto/src/imap/mod.rs create mode 100644 aero-proto/src/imap/request.rs create mode 100644 aero-proto/src/imap/response.rs create mode 100644 aero-proto/src/imap/search.rs create mode 100644 aero-proto/src/imap/session.rs create mode 100644 aero-proto/src/lib.rs create mode 100644 aero-proto/src/lmtp.rs create mode 100644 aero-proto/src/sasl.rs diff --git a/Cargo.lock b/Cargo.lock index 387615f..32b798c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,20 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "abnf-core" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182d1f071b906a9f59269c89af101515a5cbe58f723eb6717e7fe7445c0dea" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "addr2line" -version = "0.21.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" dependencies = [ "gimli", ] @@ -62,6 +71,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "aero-proto" +version = "0.3.0" +dependencies = [ + "aero-collections", + "aero-dav", + "aero-sasl", + "aero-user", + "anyhow", + "async-trait", + "base64 0.21.7", + "chrono", + "duplexify", + "eml-codec", + "futures", + "http-body-util", + "hyper 1.2.0", + "hyper-util", + "imap-codec", + "imap-flow", + "rustls 0.22.2", + "rustls-pemfile 2.1.1", + "smtp-message", + "smtp-server", + "thiserror", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util", + "tracing", +] + [[package]] name = "aero-sasl" version = "0.3.0" @@ -157,6 +197,12 @@ dependencies = [ "password-hash", ] +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "asn1-rs" version = "0.3.1" @@ -196,6 +242,208 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28243a43d821d11341ab73c80bed182dc015c514b951616cf79bd4af39af0c3" +dependencies = [ + "concurrent-queue", + "event-listener 5.2.0", + "event-listener-strategy 0.5.0", + "futures-core", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "async-executor" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ae5ebefcc48e7452b4987947920dac9450be1110cadf34d1b8c116bdbaf97c" +dependencies = [ + "async-lock 3.3.0", + "async-task", + "concurrent-queue", + "fastrand 2.0.1", + "futures-lite 2.2.0", + "slab", +] + +[[package]] +name = "async-fs" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.2.0", + "async-executor", + "async-io 2.3.1", + "async-lock 3.3.0", + "blocking", + "futures-lite 2.2.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f97ab0c5b00a7cdbe5a371b9a782ee7be1316095885c8a4ea1daf490eb0ef65" +dependencies = [ + "async-lock 3.3.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.2.0", + "parking", + "polling 3.5.0", + "rustix 0.38.31", + "slab", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +dependencies = [ + "event-listener 4.0.3", + "event-listener-strategy 0.4.0", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "async-net" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0434b1ed18ce1cf5769b8ac540e33f01fa9471058b5e89da9e06f3c882a8c12f" +dependencies = [ + "async-io 1.13.0", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "async-process" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6438ba0a08d81529c69b36700fa2f95837bfe3e776ab39cde9c14d9149da88" +dependencies = [ + "async-io 1.13.0", + "async-lock 2.8.0", + "async-signal", + "blocking", + "cfg-if", + "event-listener 3.1.0", + "futures-lite 1.13.0", + "rustix 0.38.31", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-signal" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e47d90f65a225c4527103a8d747001fc56e375203592b25ad103e1ca13124c5" +dependencies = [ + "async-io 2.3.1", + "async-lock 2.8.0", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 0.38.31", + "signal-hook-registry", + "slab", + "windows-sys 0.48.0", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.13", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" + [[package]] name = "async-trait" version = "0.1.77" @@ -207,6 +455,46 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "auto_enums" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe0dfe45d75158751e195799f47ea02e81f570aa24bc5ef999cdd9e888c4b5c3" +dependencies = [ + "auto_enums_core", + "auto_enums_derive", +] + +[[package]] +name = "auto_enums_core" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da47c46001293a2c4b744d731958be22cff408a2ab76e2279328f9713b1267b4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "auto_enums_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41aed1da83ecdc799503b7cb94da1b45a34d72b49caf40a61d9cf5b88ec07cfd" +dependencies = [ + "autocfg", + "derive_utils", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -232,7 +520,7 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", - "fastrand", + "fastrand 2.0.1", "hex", "http 0.2.12", "hyper 0.14.28", @@ -270,11 +558,11 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", - "fastrand", + "fastrand 2.0.1", "http 0.2.12", "http-body 0.4.6", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.13", "tracing", "uuid", ] @@ -433,7 +721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcf7f09a27286d84315dfb9346208abb3b0973a692454ae6d0bc8d803fcce3b4" dependencies = [ "futures-util", - "pin-project-lite", + "pin-project-lite 0.2.13", "tokio", ] @@ -452,7 +740,7 @@ dependencies = [ "http 0.2.12", "http-body 0.4.6", "md-5", - "pin-project-lite", + "pin-project-lite 0.2.13", "sha1", "sha2", "tracing", @@ -485,7 +773,7 @@ dependencies = [ "http-body 0.4.6", "once_cell", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.13", "pin-utils", "tracing", ] @@ -520,14 +808,14 @@ dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", "bytes", - "fastrand", + "fastrand 2.0.1", "h2 0.3.24", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls 0.24.2", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.13", "pin-utils", "rustls 0.21.10", "tokio", @@ -545,7 +833,7 @@ dependencies = [ "bytes", "http 0.2.12", "http 1.1.0", - "pin-project-lite", + "pin-project-lite 0.2.13", "tokio", "tracing", "zeroize", @@ -565,7 +853,7 @@ dependencies = [ "http-body 0.4.6", "itoa", "num-integer", - "pin-project-lite", + "pin-project-lite 0.2.13", "pin-utils", "ryu", "serde", @@ -600,9 +888,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", "cc", @@ -653,6 +941,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + [[package]] name = "bitmaps" version = "2.1.0" @@ -662,6 +956,18 @@ dependencies = [ "typenum", ] +[[package]] +name = "bitvec" +version = "0.19.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55f93d0ef3363c364d5976646a38f04cf67cfe1d4c8d160cdea02cab2c116b33" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.10.6" @@ -680,6 +986,42 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" +dependencies = [ + "async-channel 2.2.0", + "async-lock 3.3.0", + "async-task", + "fastrand 2.0.1", + "futures-io", + "futures-lite 2.2.0", + "piper", + "tracing", +] + +[[package]] +name = "bounded-static" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2325bd33fa7e3018e7e37f5b0591ba009124963b5a3f8b7cae6d0a8c1028ed4" +dependencies = [ + "bounded-static-derive", +] + +[[package]] +name = "bounded-static-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10dd247355bf631d98d2753d87ae62c84c8dcb996ad9b24a4168e0aec29bd6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -738,6 +1080,15 @@ dependencies = [ "windows-targets 0.52.0", ] +[[package]] +name = "concurrent-queue" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -787,6 +1138,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + [[package]] name = "crypto-bigint" version = "0.4.9" @@ -869,6 +1226,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "derive_utils" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b4c15dccee12c7044f1fcad956e98410860b22231e44a3b827464797ca7bf" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "digest" version = "0.10.7" @@ -891,6 +1259,16 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "duplexify" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1cc346cd6db38ceab2d33f59b26024c3ddb8e75f047c6cafbcbc016ea8065d5" +dependencies = [ + "async-std", + "pin-project-lite 0.1.12", +] + [[package]] name = "ecdsa" version = "0.14.8" @@ -965,6 +1343,84 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93877bcde0eb80ca09131a08d23f0a5c18a620b01db137dba666d18cd9b30c2" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener-strategy" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +dependencies = [ + "event-listener 4.0.3", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +dependencies = [ + "event-listener 5.2.0", + "pin-project-lite 0.2.13", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.1" @@ -996,6 +1452,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + [[package]] name = "futures" version = "0.3.30" @@ -1044,6 +1506,34 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite 0.2.13", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +dependencies = [ + "fastrand 2.0.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite 0.2.13", +] + [[package]] name = "futures-macro" version = "0.3.30" @@ -1080,7 +1570,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.13", "pin-utils", "slab", ] @@ -1108,9 +1598,21 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" + +[[package]] +name = "gloo-timers" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] [[package]] name = "group" @@ -1218,7 +1720,7 @@ checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", - "pin-project-lite", + "pin-project-lite 0.2.13", ] [[package]] @@ -1241,7 +1743,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.0", - "pin-project-lite", + "pin-project-lite 0.2.13", ] [[package]] @@ -1272,8 +1774,8 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite", - "socket2", + "pin-project-lite 0.2.13", + "socket2 0.5.5", "tokio", "tower-service", "tracing", @@ -1295,7 +1797,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite", + "pin-project-lite 0.2.13", "smallvec", "tokio", "want", @@ -1348,8 +1850,8 @@ dependencies = [ "http 1.1.0", "http-body 1.0.0", "hyper 1.2.0", - "pin-project-lite", - "socket2", + "pin-project-lite 0.2.13", + "socket2 0.5.5", "tokio", "tower", "tower-service", @@ -1379,6 +1881,17 @@ dependencies = [ "cc", ] +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -1403,6 +1916,46 @@ dependencies = [ "version_check", ] +[[package]] +name = "imap-codec" +version = "2.0.0" +source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed" +dependencies = [ + "abnf-core", + "base64 0.21.7", + "bounded-static", + "chrono", + "imap-types", + "log", + "nom 7.1.3", + "thiserror", +] + +[[package]] +name = "imap-flow" +version = "0.1.0" +source = "git+https://github.com/duesee/imap-flow.git?branch=main#dce759a8531f317e8d7311fb032b366db6698e38" +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#d8a5afc03fb771232e94c73af6a05e79dc80bbed" +dependencies = [ + "base64 0.21.7", + "bounded-static", + "chrono", + "thiserror", +] + [[package]] name = "indexmap" version = "2.2.5" @@ -1413,6 +1966,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "itoa" version = "1.0.10" @@ -1460,6 +2033,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -1504,6 +2086,19 @@ dependencies = [ "x509-parser", ] +[[package]] +name = "lexical-core" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "cfg-if", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.152" @@ -1533,11 +2128,32 @@ dependencies = [ "walkdir", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "md-5" @@ -1551,9 +2167,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "minimal-lexical" @@ -1563,11 +2179,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", + "autocfg", ] [[package]] @@ -1587,6 +2204,19 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf51a729ecf40266a2368ad335a5fdde43471f545a967109cd62146ecf8b66ff" +[[package]] +name = "nom" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6a7a9657c84d5814c6196b68bb4429df09c18b1573806259fba397ea4ad0d44" +dependencies = [ + "bitvec", + "funty", + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "nom" version = "7.1.3" @@ -1639,12 +2269,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" -dependencies = [ - "memchr", -] +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "oid-registry" @@ -1684,6 +2311,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + [[package]] name = "password-hash" version = "0.5.0" @@ -1727,6 +2360,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1739,6 +2378,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" +dependencies = [ + "atomic-waker", + "fastrand 2.0.1", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -1755,6 +2405,36 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite 0.2.13", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f040dee2588b4963afb4e420540439d126f73fdacf4a9c486a96d840bac3c9" +dependencies = [ + "cfg-if", + "concurrent-queue", + "pin-project-lite 0.2.13", + "rustix 0.38.31", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1795,6 +2475,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" + [[package]] name = "rand" version = "0.8.5" @@ -1834,12 +2520,27 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-lite" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b661b2f27137bdbc16f00eda72866a92bb28af1753ffbd56744fb6e2e9cd8e" +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "rfc6979" version = "0.3.1" @@ -1926,6 +2627,33 @@ dependencies = [ "nom 7.1.3", ] +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +dependencies = [ + "bitflags 2.4.2", + "errno", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + [[package]] name = "rustls" version = "0.20.9" @@ -2089,7 +2817,7 @@ version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2209,6 +2937,71 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "smol" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13f2b548cd8447f8de0fdf1c592929f70f4fc7039a05e47404b0d096ec6987a1" +dependencies = [ + "async-channel 1.9.0", + "async-executor", + "async-fs", + "async-io 1.13.0", + "async-lock 2.8.0", + "async-net", + "async-process", + "blocking", + "futures-lite 1.13.0", +] + +[[package]] +name = "smtp-message" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111" +dependencies = [ + "auto_enums", + "futures", + "idna 0.2.3", + "lazy_static", + "nom 6.2.2", + "pin-project", + "regex-automata", + "serde", +] + +[[package]] +name = "smtp-server" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111" +dependencies = [ + "async-trait", + "chrono", + "duplexify", + "futures", + "smol", + "smtp-message", + "smtp-server-types", +] + +[[package]] +name = "smtp-server-types" +version = "0.1.0" +source = "git+http://github.com/Alexis211/kannader?branch=feature/lmtp#0560e7c46af752344a3095add5f84b02400b1111" +dependencies = [ + "serde", + "smtp-message", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.5" @@ -2253,6 +3046,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.5.0" @@ -2293,6 +3092,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "1.0.56" @@ -2368,9 +3173,9 @@ dependencies = [ "libc", "mio", "num_cpus", - "pin-project-lite", + "pin-project-lite 0.2.13", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys 0.48.0", ] @@ -2425,7 +3230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite", + "pin-project-lite 0.2.13", "tokio", ] @@ -2439,7 +3244,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite", + "pin-project-lite 0.2.13", "tokio", "tracing", ] @@ -2462,7 +3267,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project", - "pin-project-lite", + "pin-project-lite 0.2.13", "tokio", "tower-layer", "tower-service", @@ -2488,7 +3293,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", - "pin-project-lite", + "pin-project-lite 0.2.13", "tracing-attributes", "tracing-core", ] @@ -2571,7 +3376,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", - "idna", + "idna 0.5.0", "percent-encoding", ] @@ -2587,6 +3392,12 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +[[package]] +name = "value-bag" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126e423afe2dd9ac52142e7e9d5ce4135d7e13776c529d27fd6bc49f19e3280b" + [[package]] name = "version_check" version = "0.9.4" @@ -2599,6 +3410,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "waker-fn" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c4517f54858c779bbcbf228f4fca63d121bf85fbecb2dc578cdf4a39395690" + [[package]] name = "walkdir" version = "2.4.0" @@ -2649,6 +3466,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.90" @@ -2870,6 +3699,12 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + [[package]] name = "x509-parser" version = "0.13.2" @@ -2928,13 +3763,3 @@ dependencies = [ "cc", "libc", ] - -[[patch.unused]] -name = "imap-codec" -version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed" - -[[patch.unused]] -name = "imap-types" -version = "2.0.0" -source = "git+https://github.com/superboum/imap-codec?branch=custom/aerogramme#d8a5afc03fb771232e94c73af6a05e79dc80bbed" diff --git a/Cargo.toml b/Cargo.toml index 5654322..406d5bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ members = [ "aero-dav", "aero-dav/fuzz", "aero-collections", -# "aero-proto", + "aero-proto", # "aerogramme", ] @@ -20,7 +20,7 @@ aero-bayou = { version = "0.3.0", path = "aero-bayou" } aero-sasl = { version = "0.3.0", path = "aero-sasl" } aero-dav = { version = "0.3.0", path = "aero-dav" } aero-collections = { version = "0.3.0", path = "aero-collections" } -#aero-proto = { version = "0.3.0", path = "aero-proto" } +aero-proto = { version = "0.3.0", path = "aero-proto" } #aerogramme = { version = "0.3.0", path = "aerogramme" } # async runtime diff --git a/aero-dav/src/caldecoder.rs b/aero-dav/src/caldecoder.rs index 5f40c4b..fb840d6 100644 --- a/aero-dav/src/caldecoder.rs +++ b/aero-dav/src/caldecoder.rs @@ -1,4 +1,4 @@ -use super::types as dav; +//use super::types as dav; use super::caltypes::*; use super::xml; use super::error; @@ -7,25 +7,25 @@ use super::error; // ---- EXTENSIONS --- impl xml::QRead for Violation { - async fn qread(xml: &mut xml::Reader) -> Result { + async fn qread(_xml: &mut xml::Reader) -> Result { unreachable!(); } } impl xml::QRead for Property { - async fn qread(xml: &mut xml::Reader) -> Result { + async fn qread(_xml: &mut xml::Reader) -> Result { unreachable!(); } } impl xml::QRead for PropertyRequest { - async fn qread(xml: &mut xml::Reader) -> Result { + async fn qread(_xml: &mut xml::Reader) -> Result { unreachable!(); } } impl xml::QRead for ResourceType { - async fn qread(xml: &mut xml::Reader) -> Result { + async fn qread(_xml: &mut xml::Reader) -> Result { unreachable!(); } } diff --git a/aero-dav/src/calencoder.rs b/aero-dav/src/calencoder.rs index ff6eb24..67892ed 100644 --- a/aero-dav/src/calencoder.rs +++ b/aero-dav/src/calencoder.rs @@ -1,7 +1,5 @@ use quick_xml::Error as QError; -use quick_xml::events::{Event, BytesEnd, BytesStart, BytesText}; -use quick_xml::name::PrefixDeclaration; -use tokio::io::AsyncWrite; +use quick_xml::events::{Event, BytesText}; use super::caltypes::*; use super::xml::{Node, QWrite, IWrite, Writer}; @@ -627,7 +625,7 @@ impl QWrite for ParamFilterMatch { impl QWrite for TimeZone { async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { - let mut start = xml.create_cal_element("timezone"); + let start = xml.create_cal_element("timezone"); let end = start.to_end(); xml.q.write_event_async(Event::Start(start.clone())).await?; @@ -638,7 +636,7 @@ impl QWrite for TimeZone { impl QWrite for Filter { async fn qwrite(&self, xml: &mut Writer) -> Result<(), QError> { - let mut start = xml.create_cal_element("filter"); + let start = xml.create_cal_element("filter"); let end = start.to_end(); xml.q.write_event_async(Event::Start(start.clone())).await?; diff --git a/aero-dav/src/decoder.rs b/aero-dav/src/decoder.rs index 65cb712..02bc376 100644 --- a/aero-dav/src/decoder.rs +++ b/aero-dav/src/decoder.rs @@ -1,14 +1,9 @@ -use std::future::Future; - use quick_xml::events::Event; -use quick_xml::events::attributes::AttrError; -use quick_xml::name::{Namespace, QName, PrefixDeclaration, ResolveResult, ResolveResult::*}; -use quick_xml::reader::NsReader; -use tokio::io::AsyncBufRead; +use chrono::DateTime; use super::types::*; use super::error::ParsingError; -use super::xml::{Node, QRead, Reader, IRead, DAV_URN, CAL_URN}; +use super::xml::{Node, QRead, Reader, IRead, DAV_URN}; //@TODO (1) Rewrite all objects as Href, // where we return Ok(None) instead of trying to find the object at any cost. @@ -119,7 +114,7 @@ impl QRead for LockInfo { impl QRead> for PropValue { async fn qread(xml: &mut Reader) -> Result { xml.open(DAV_URN, "prop").await?; - let mut acc = xml.collect::>().await?; + let acc = xml.collect::>().await?; xml.close().await?; Ok(PropValue(acc)) } @@ -352,8 +347,6 @@ impl QRead> for PropertyRequest { impl QRead> for Property { async fn qread(xml: &mut Reader) -> Result { - use chrono::{DateTime, FixedOffset, TimeZone}; - // Core WebDAV properties if xml.maybe_open(DAV_URN, "creationdate").await?.is_some() { let datestr = xml.tag_string().await?; @@ -592,7 +585,7 @@ impl QRead for LockType { impl QRead for Href { async fn qread(xml: &mut Reader) -> Result { xml.open(DAV_URN, "href").await?; - let mut url = xml.tag_string().await?; + let url = xml.tag_string().await?; xml.close().await?; Ok(Href(url)) } diff --git a/aero-dav/src/lib.rs b/aero-dav/src/lib.rs index 6bfbf62..0ca8243 100644 --- a/aero-dav/src/lib.rs +++ b/aero-dav/src/lib.rs @@ -1,5 +1,4 @@ #![feature(type_alias_impl_trait)] -#![feature(async_fn_in_trait)] #![feature(async_closure)] #![feature(trait_alias)] diff --git a/aero-dav/src/realization.rs b/aero-dav/src/realization.rs index 33a556e..5781637 100644 --- a/aero-dav/src/realization.rs +++ b/aero-dav/src/realization.rs @@ -6,12 +6,12 @@ use super::error; #[derive(Debug, PartialEq)] pub struct Disabled(()); impl xml::QRead for Disabled { - async fn qread(xml: &mut xml::Reader) -> Result { + async fn qread(_xml: &mut xml::Reader) -> Result { Err(error::ParsingError::Recoverable) } } impl xml::QWrite for Disabled { - async fn qwrite(&self, xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { + async fn qwrite(&self, _xml: &mut xml::Writer) -> Result<(), quick_xml::Error> { unreachable!(); } } diff --git a/aero-proto/Cargo.toml b/aero-proto/Cargo.toml new file mode 100644 index 0000000..df8c696 --- /dev/null +++ b/aero-proto/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "aero-proto" +version = "0.3.0" +authors = ["Alex Auvolat ", "Quentin Dufour "] +edition = "2021" +license = "EUPL-1.2" +description = "Binding between Aerogramme's internal components and well-known protocols" + +[dependencies] +aero-sasl.workspace = true +aero-dav.workspace = true +aero-user.workspace = true +aero-collections.workspace = true + +async-trait.workspace = true +anyhow.workspace = true +hyper.workspace = true +base64.workspace = true +hyper-util.workspace = true +http-body-util.workspace = true +futures.workspace = true +tokio.workspace = true +tokio-util.workspace = true +tokio-rustls.workspace = true +rustls.workspace = true +rustls-pemfile.workspace = true +imap-codec.workspace = true +imap-flow.workspace = true +chrono.workspace = true +eml-codec.workspace = true +thiserror.workspace = true +duplexify.workspace = true +smtp-message.workspace = true +smtp-server.workspace = true +tracing.workspace = true diff --git a/aero-proto/dav.rs b/aero-proto/dav.rs deleted file mode 100644 index fa2023a..0000000 --- a/aero-proto/dav.rs +++ /dev/null @@ -1,145 +0,0 @@ -use std::net::SocketAddr; - -use anyhow::{anyhow, Result}; -use base64::Engine; -use hyper::service::service_fn; -use hyper::{Request, Response, body::Bytes}; -use hyper::server::conn::http1 as http; -use hyper_util::rt::TokioIo; -use http_body_util::Full; -use futures::stream::{FuturesUnordered, StreamExt}; -use tokio::net::TcpListener; -use tokio::sync::watch; - -use crate::config::DavUnsecureConfig; -use crate::login::ArcLoginProvider; -use crate::user::User; - -pub struct Server { - bind_addr: SocketAddr, - login_provider: ArcLoginProvider, -} - -pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server { - Server { - bind_addr: config.bind_addr, - login_provider: login, - } -} - -impl Server { - pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!("DAV server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - tracing::info!("Accepted connection from {}", remote_addr); - let stream = TokioIo::new(socket); - let login = self.login_provider.clone(); - let conn = tokio::spawn(async move { - //@FIXME should create a generic "public web" server on which "routers" could be - //abitrarily bound - //@FIXME replace with a handler supporting http2 and TLS - match http::Builder::new().serve_connection(stream, service_fn(|req: Request| { - let login = login.clone(); - async move { - auth(login, req).await - } - })).await { - Err(e) => tracing::warn!(err=?e, "connection failed"), - Ok(()) => tracing::trace!("connection terminated with success"), - } - }); - connections.push(conn); - } - drop(tcp); - - tracing::info!("Server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -//@FIXME We should not support only BasicAuth -async fn auth( - login: ArcLoginProvider, - req: Request, -) -> Result>> { - - let auth_val = match req.headers().get("Authorization") { - Some(hv) => hv.to_str()?, - None => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Missing Authorization field")))?), - }; - - let b64_creds_maybe_padded = match auth_val.split_once(" ") { - Some(("Basic", b64)) => b64, - _ => return Ok(Response::builder() - .status(400) - .body(Full::new(Bytes::from("Unsupported Authorization field")))?), - }; - - // base64urlencoded may have trailing equals, base64urlsafe has not - // theoretically authorization is padded but "be liberal in what you accept" - let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('='); - - // Decode base64 - let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?; - let str_creds = std::str::from_utf8(&creds)?; - - // Split username and password - let (username, password) = str_creds - .split_once(':') - .ok_or(anyhow!("Missing colon in Authorization, can't split decoded value into a username/password pair"))?; - - // Call login provider - let creds = match login.login(username, password).await { - Ok(c) => c, - Err(e) => return Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::from("Wrong credentials")))?), - }; - - // Build a user - let user = User::new(username.into(), creds).await?; - - // Call router with user - router(user, req).await -} - -async fn router(user: std::sync::Arc, req: Request) -> Result>> { - let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect(); - match path_segments.as_slice() { - [] => tracing::info!("root"), - [ username, ..] if *username != user.username => return Ok(Response::builder() - .status(403) - .body(Full::new(Bytes::from("Accessing other user ressources is not allowed")))?), - [ _ ] => tracing::info!("user home"), - [ _, "calendar" ] => tracing::info!("user calendars"), - [ _, "calendar", colname ] => tracing::info!(name=colname, "selected calendar"), - [ _, "calendar", colname, member ] => tracing::info!(name=colname, obj=member, "selected event"), - _ => return Ok(Response::builder() - .status(404) - .body(Full::new(Bytes::from("Resource not found")))?), - } - Ok(Response::new(Full::new(Bytes::from("Hello World!")))) -} - -async fn collections(user: std::sync::Arc, req: Request) -> Result>> { - unimplemented!(); -} diff --git a/aero-proto/imap/attributes.rs b/aero-proto/imap/attributes.rs deleted file mode 100644 index 89446a8..0000000 --- a/aero-proto/imap/attributes.rs +++ /dev/null @@ -1,77 +0,0 @@ -use imap_codec::imap_types::command::FetchModifier; -use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}; - -/// Internal decisions based on fetched attributes -/// passed by the client - -pub struct AttributesProxy { - pub attrs: Vec>, -} -impl AttributesProxy { - pub fn new( - attrs: &MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - is_uid_fetch: bool, - ) -> Self { - // Expand macros - let mut fetch_attrs = match attrs { - MacroOrMessageDataItemNames::Macro(m) => { - use imap_codec::imap_types::fetch::Macro; - use MessageDataItemName::*; - match m { - Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope], - Macro::Fast => vec![Flags, InternalDate, Rfc822Size], - Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], - _ => { - tracing::error!("unimplemented macro"); - vec![] - } - } - } - MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), - }; - - // Handle uids - if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { - fetch_attrs.push(MessageDataItemName::Uid); - } - - // Handle inferred MODSEQ tag - let is_changed_since = modifiers - .iter() - .any(|m| matches!(m, FetchModifier::ChangedSince(..))); - if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) { - fetch_attrs.push(MessageDataItemName::ModSeq); - } - - Self { attrs: fetch_attrs } - } - - pub fn is_enabling_condstore(&self) -> bool { - self.attrs - .iter() - .any(|x| matches!(x, MessageDataItemName::ModSeq)) - } - - pub fn need_body(&self) -> bool { - self.attrs.iter().any(|x| match x { - MessageDataItemName::Body - | MessageDataItemName::Rfc822 - | MessageDataItemName::Rfc822Text - | MessageDataItemName::BodyStructure => true, - - MessageDataItemName::BodyExt { - section: Some(section), - partial: _, - peek: _, - } => match section { - Section::Header(None) - | Section::HeaderFields(None, _) - | Section::HeaderFieldsNot(None, _) => false, - _ => true, - }, - MessageDataItemName::BodyExt { .. } => true, - _ => false, - }) - } -} diff --git a/aero-proto/imap/capability.rs b/aero-proto/imap/capability.rs deleted file mode 100644 index c76b51c..0000000 --- a/aero-proto/imap/capability.rs +++ /dev/null @@ -1,159 +0,0 @@ -use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier}; -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; - -use crate::imap::attributes::AttributesProxy; - -fn capability_unselect() -> Capability<'static> { - Capability::try_from("UNSELECT").unwrap() -} - -fn capability_condstore() -> Capability<'static> { - Capability::try_from("CONDSTORE").unwrap() -} - -fn capability_uidplus() -> Capability<'static> { - Capability::try_from("UIDPLUS").unwrap() -} - -fn capability_liststatus() -> Capability<'static> { - Capability::try_from("LIST-STATUS").unwrap() -} - -/* -fn capability_qresync() -> Capability<'static> { - Capability::try_from("QRESYNC").unwrap() -} -*/ - -#[derive(Debug, Clone)] -pub struct ServerCapability(HashSet>); - -impl Default for ServerCapability { - fn default() -> Self { - Self(HashSet::from([ - Capability::Imap4Rev1, - Capability::Enable, - Capability::Move, - Capability::LiteralPlus, - Capability::Idle, - capability_unselect(), - capability_condstore(), - capability_uidplus(), - capability_liststatus(), - //capability_qresync(), - ])) - } -} - -impl ServerCapability { - pub fn to_vec(&self) -> Vec1> { - self.0 - .iter() - .map(|v| v.clone()) - .collect::>() - .try_into() - .unwrap() - } - - #[allow(dead_code)] - pub fn support(&self, cap: &Capability<'static>) -> bool { - self.0.contains(cap) - } -} - -#[derive(Clone)] -pub enum ClientStatus { - NotSupportedByServer, - Disabled, - Enabled, -} -impl ClientStatus { - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Enabled) - } - - pub fn enable(&self) -> Self { - match self { - Self::Disabled => Self::Enabled, - other => other.clone(), - } - } -} - -pub struct ClientCapability { - pub condstore: ClientStatus, - pub utf8kind: Option, -} - -impl ClientCapability { - pub fn new(sc: &ServerCapability) -> Self { - Self { - condstore: match sc.0.contains(&capability_condstore()) { - true => ClientStatus::Disabled, - _ => ClientStatus::NotSupportedByServer, - }, - utf8kind: None, - } - } - - pub fn enable_condstore(&mut self) { - self.condstore = self.condstore.enable(); - } - - pub fn attributes_enable(&mut self, ap: &AttributesProxy) { - if ap.is_enabling_condstore() { - self.enable_condstore() - } - } - - pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) { - if mods - .iter() - .any(|x| matches!(x, FetchModifier::ChangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) { - if mods - .iter() - .any(|x| matches!(x, StoreModifier::UnchangedSince(..))) - { - self.enable_condstore() - } - } - - pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { - for m in mods.iter() { - match m { - SelectExamineModifier::Condstore => self.enable_condstore(), - } - } - } - - pub fn try_enable( - &mut self, - caps: &[CapabilityEnable<'static>], - ) -> Vec> { - let mut enabled = vec![]; - for cap in caps { - match cap { - CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { - self.condstore = ClientStatus::Enabled; - enabled.push(cap.clone()); - } - CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { - self.utf8kind = Some(kind.clone()); - enabled.push(cap.clone()); - } - _ => (), - } - } - - enabled - } -} diff --git a/aero-proto/imap/command/anonymous.rs b/aero-proto/imap/command/anonymous.rs deleted file mode 100644 index 811d1e4..0000000 --- a/aero-proto/imap/command/anonymous.rs +++ /dev/null @@ -1,83 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody}; -use imap_codec::imap_types::core::AString; -use imap_codec::imap_types::response::Code; -use imap_codec::imap_types::secret::Secret; - -use crate::imap::capability::ServerCapability; -use crate::imap::command::anystate; -use crate::imap::flow; -use crate::imap::response::Response; -use crate::login::ArcLoginProvider; -use crate::user::User; - -//--- dispatching - -pub struct AnonymousContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub login_provider: &'a ArcLoginProvider, -} - -pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to anonymous context (3 commands) - CommandBody::Login { username, password } => ctx.login(username, password).await, - CommandBody::Authenticate { .. } => { - anystate::not_implemented(ctx.req.tag.clone(), "authenticate") - } - //StartTLS is not implemented for now, we will probably go full TLS. - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -//--- Command controllers, private - -impl<'a> AnonymousContext<'a> { - async fn login( - self, - username: &AString<'a>, - password: &Secret>, - ) -> Result<(Response<'static>, flow::Transition)> { - let (u, p) = ( - std::str::from_utf8(username.as_ref())?, - std::str::from_utf8(password.declassify().as_ref())?, - ); - tracing::info!(user = %u, "command.login"); - - let creds = match self.login_provider.login(&u, &p).await { - Err(e) => { - tracing::debug!(error=%e, "authentication failed"); - return Ok(( - Response::build() - .to_req(self.req) - .message("Authentication failed") - .no()?, - flow::Transition::None, - )); - } - Ok(c) => c, - }; - - let user = User::new(u.to_string(), creds).await?; - - tracing::info!(username=%u, "connected"); - Ok(( - Response::build() - .to_req(self.req) - .code(Code::Capability(self.server_capabilities.to_vec())) - .message("Completed") - .ok()?, - flow::Transition::Authenticate(user), - )) - } -} diff --git a/aero-proto/imap/command/anystate.rs b/aero-proto/imap/command/anystate.rs deleted file mode 100644 index 718ba3f..0000000 --- a/aero-proto/imap/command/anystate.rs +++ /dev/null @@ -1,54 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::Data; - -use crate::imap::capability::ServerCapability; -use crate::imap::flow; -use crate::imap::response::Response; - -pub(crate) fn capability( - tag: Tag<'static>, - cap: &ServerCapability, -) -> Result<(Response<'static>, flow::Transition)> { - let res = Response::build() - .tag(tag) - .message("Server capabilities") - .data(Data::Capability(cap.to_vec())) - .ok()?; - - Ok((res, flow::Transition::None)) -} - -pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build().tag(tag).message("Noop completed.").ok()?, - flow::Transition::None, - )) -} - -pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { - Ok((Response::bye()?, flow::Transition::Logout)) -} - -pub(crate) fn not_implemented<'a>( - tag: Tag<'a>, - what: &str, -) -> Result<(Response<'a>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message(format!("Command not implemented {}", what)) - .bad()?, - flow::Transition::None, - )) -} - -pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .tag(tag) - .message("Command not authorized in this state") - .bad()?, - flow::Transition::None, - )) -} diff --git a/aero-proto/imap/command/authenticated.rs b/aero-proto/imap/command/authenticated.rs deleted file mode 100644 index 3d332ec..0000000 --- a/aero-proto/imap/command/authenticated.rs +++ /dev/null @@ -1,683 +0,0 @@ -use std::collections::BTreeMap; -use std::sync::Arc; -use thiserror::Error; - -use anyhow::{anyhow, bail, Result}; -use imap_codec::imap_types::command::{ - Command, CommandBody, ListReturnItem, SelectExamineModifier, -}; -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}; -use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; -use imap_codec::imap_types::response::{Code, CodeOther, Data}; -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, UpdateParameters}; -use crate::imap::response::Response; -use crate::imap::Body; - -use crate::mail::uidindex::*; -use crate::user::User; -use crate::mail::IMF; -use crate::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW; - -pub struct AuthenticatedContext<'a> { - pub req: &'a Command<'static>, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub user: &'a Arc, -} - -pub async fn dispatch<'a>( - mut ctx: AuthenticatedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any state - CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (11 commands) - CommandBody::Create { mailbox } => ctx.create(mailbox).await, - CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, - CommandBody::Rename { from, to } => ctx.rename(from, to).await, - CommandBody::Lsub { - reference, - mailbox_wildcard, - } => ctx.list(reference, mailbox_wildcard, &[], true).await, - CommandBody::List { - reference, - mailbox_wildcard, - r#return, - } => ctx.list(reference, mailbox_wildcard, r#return, false).await, - CommandBody::Status { - mailbox, - item_names, - } => ctx.status(mailbox, item_names).await, - CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, - CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, - CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, - CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, - CommandBody::Append { - mailbox, - flags, - date, - message, - } => ctx.append(mailbox, flags, date, message).await, - - // rfc5161 ENABLE - CommandBody::Enable { capabilities } => ctx.enable(capabilities), - - // Collect other commands - _ => anystate::wrong_state(ctx.req.tag.clone()), - } -} - -// --- PRIVATE --- -impl<'a> AuthenticatedContext<'a> { - async fn create( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name = match mailbox { - MailboxCodec::Inbox => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Cannot create INBOX") - .bad()?, - flow::Transition::None, - )); - } - MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, - }; - - match self.user.create_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("CREATE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(&e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn delete( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - match self.user.delete_mailbox(&name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("DELETE complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn rename( - self, - from: &MailboxCodec<'a>, - to: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(from).try_into()?; - let new_name: &str = MailboxName(to).try_into()?; - - match self.user.rename_mailbox(&name, &new_name).await { - Ok(()) => Ok(( - Response::build() - .to_req(self.req) - .message("RENAME complete") - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - async fn list( - &mut self, - reference: &MailboxCodec<'a>, - mailbox_wildcard: &ListMailbox<'a>, - must_return: &[ListReturnItem], - is_lsub: bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); - - let reference: &str = MailboxName(reference).try_into()?; - if !reference.is_empty() { - return Ok(( - Response::build() - .to_req(self.req) - .message("References not supported") - .bad()?, - flow::Transition::None, - )); - } - - let status_item_names = must_return.iter().find_map(|m| match m { - ListReturnItem::Status(v) => Some(v), - _ => None, - }); - - // @FIXME would probably need a rewrite to better use the imap_codec library - let wildcard = match mailbox_wildcard { - ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, - ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, - }; - if wildcard.is_empty() { - if is_lsub { - return Ok(( - Response::build() - .to_req(self.req) - .message("LSUB complete") - .data(Data::Lsub { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } else { - return Ok(( - Response::build() - .to_req(self.req) - .message("LIST complete") - .data(Data::List { - items: vec![], - delimiter: Some(mbx_hier_delim), - mailbox: "".try_into().unwrap(), - }) - .ok()?, - flow::Transition::None, - )); - } - } - - let mailboxes = self.user.list_mailboxes().await?; - let mut vmailboxes = BTreeMap::new(); - for mb in mailboxes.iter() { - for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { - if i > 0 { - let smb = &mb[..i]; - vmailboxes.entry(smb).or_insert(false); - } - } - vmailboxes.insert(mb, true); - } - - let mut ret = vec![]; - for (mb, is_real) in vmailboxes.iter() { - if matches_wildcard(&wildcard, mb) { - let mailbox: MailboxCodec = mb - .to_string() - .try_into() - .map_err(|_| anyhow!("invalid mailbox name"))?; - let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))]; - - // Decoration - if !*is_real { - items.push(FlagNameAttribute::Noselect); - } else { - match *mb { - "Drafts" => items.push(Atom::unvalidated("Drafts").into()), - "Archive" => items.push(Atom::unvalidated("Archive").into()), - "Sent" => items.push(Atom::unvalidated("Sent").into()), - "Trash" => items.push(Atom::unvalidated("Trash").into()), - _ => (), - }; - } - - // Result type - if is_lsub { - ret.push(Data::Lsub { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } else { - ret.push(Data::List { - items, - delimiter: Some(mbx_hier_delim), - mailbox: mailbox.clone(), - }); - } - - // Also collect status - if let Some(sin) = status_item_names { - let ret_attrs = match self.status_items(mb, sin).await { - Ok(a) => a, - Err(e) => { - tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox"); - continue; - } - }; - - let data = Data::Status { - mailbox, - items: ret_attrs.into(), - }; - - ret.push(data); - } - } - } - - let msg = if is_lsub { - "LSUB completed" - } else { - "LIST completed" - }; - Ok(( - Response::build() - .to_req(self.req) - .message(msg) - .many_data(ret) - .ok()?, - flow::Transition::None, - )) - } - - async fn status( - &mut self, - mailbox: &MailboxCodec<'static>, - attributes: &[StatusDataItemName], - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let ret_attrs = match self.status_items(name, attributes).await { - Ok(v) => v, - Err(e) => match e.downcast_ref::() { - Some(CommandError::MailboxNotFound) => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - _ => return Err(e.into()), - }, - }; - - let data = Data::Status { - mailbox: mailbox.clone(), - items: ret_attrs.into(), - }; - - Ok(( - Response::build() - .to_req(self.req) - .message("STATUS completed") - .data(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn status_items( - &mut self, - name: &str, - attributes: &[StatusDataItemName], - ) -> Result> { - let mb_opt = self.user.open_mailbox(name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => return Err(CommandError::MailboxNotFound.into()), - }; - - let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - - let mut ret_attrs = vec![]; - for attr in attributes.iter() { - ret_attrs.push(match attr { - StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), - StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), - StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), - StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), - StatusDataItemName::UidValidity => { - StatusDataItem::UidValidity(view.uidvalidity()) - } - StatusDataItemName::Deleted => { - bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); - }, - StatusDataItemName::DeletedStorage => { - bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); - }, - StatusDataItemName::HighestModSeq => { - self.client_capabilities.enable_condstore(); - StatusDataItem::HighestModSeq(view.highestmodseq().get()) - }, - }); - } - Ok(ret_attrs) - } - - async fn subscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message("SUBSCRIBE complete") - .ok()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .bad()?, - flow::Transition::None, - )) - } - } - - async fn unsubscribe( - self, - mailbox: &MailboxCodec<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let name: &str = MailboxName(mailbox).try_into()?; - - if self.user.has_mailbox(&name).await? { - Ok(( - Response::build() - .to_req(self.req) - .message(format!( - "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", - name - )) - .bad()?, - flow::Transition::None, - )) - } else { - Ok(( - Response::build() - .to_req(self.req) - .message(format!("Mailbox {} does not exist", name)) - .no()?, - flow::Transition::None, - )) - } - } - - /* - * TRACE BEGIN --- - - - Example: C: A142 SELECT INBOX - S: * 172 EXISTS - S: * 1 RECENT - S: * OK [UNSEEN 12] Message 12 is first unseen - S: * OK [UIDVALIDITY 3857529045] UIDs valid - S: * OK [UIDNEXT 4392] Predicted next UID - S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) - S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited - S: A142 OK [READ-WRITE] SELECT completed - - --- a mailbox with no unseen message -> no unseen entry - NOTES: - RFC3501 (imap4rev1) says if there is no OK [UNSEEN] response, client must make no assumption, - it is therefore correct to not return it even if there are unseen messages - RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE - For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. - - - 20 select "INBOX.achats" - * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) - * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted. - * 88 EXISTS - * 0 RECENT - * OK [UIDVALIDITY 1347986788] UIDs valid - * OK [UIDNEXT 91] Predicted next UID - * OK [HIGHESTMODSEQ 72] Highest - 20 OK [READ-WRITE] Select completed (0.001 + 0.000 secs). - - * TRACE END --- - */ - async fn select( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .message("Select completed") - .to_req(self.req) - .code(Code::ReadWrite) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite), - )) - } - - async fn examine( - self, - mailbox: &MailboxCodec<'a>, - modifiers: &[SelectExamineModifier], - ) -> Result<(Response<'static>, flow::Transition)> { - self.client_capabilities.select_enable(modifiers); - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Mailbox does not exist") - .no()?, - flow::Transition::None, - )) - } - }; - tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); - - let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; - let data = mb.summary()?; - - Ok(( - Response::build() - .to_req(self.req) - .message("Examine completed") - .code(Code::ReadOnly) - .set_body(data) - .ok()?, - flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly), - )) - } - - //@FIXME we should write a specific version for the "selected" state - //that returns some unsollicited responses - async fn append( - self, - mailbox: &MailboxCodec<'a>, - flags: &[Flag<'a>], - date: &Option, - message: &Literal<'a>, - ) -> Result<(Response<'static>, flow::Transition)> { - let append_tag = self.req.tag.clone(); - match self.append_internal(mailbox, flags, date, message).await { - Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok(( - Response::build() - .tag(append_tag) - .message("APPEND completed") - .code(Code::Other(CodeOther::unvalidated( - format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )), - Err(e) => Ok(( - Response::build() - .tag(append_tag) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - fn enable( - self, - cap_enable: &Vec1>, - ) -> 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()); - if capabilities.len() > 0 { - response_builder = response_builder.data(Data::Enabled { capabilities }); - } - Ok(( - response_builder.message("ENABLE completed").ok()?, - flow::Transition::None, - )) - } - - //@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, - message: &Literal<'a>, - ) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> { - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - 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"); - } - - let msg = - IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; - let flags = flags.iter().map(|x| x.to_string()).collect::>(); - // TODO: filter allowed flags? ping @Quentin - - let (uidvalidity, uid, modseq) = - view.internal.mailbox.append(msg, None, &flags[..]).await?; - //let unsollicited = view.update(UpdateParameters::default()).await?; - - Ok((view, uidvalidity, uid, modseq)) - } -} - -fn matches_wildcard(wildcard: &str, name: &str) -> bool { - let wildcard = wildcard.chars().collect::>(); - let name = name.chars().collect::>(); - - let mut matches = vec![vec![false; wildcard.len() + 1]; name.len() + 1]; - - for i in 0..=name.len() { - for j in 0..=wildcard.len() { - matches[i][j] = (i == 0 && j == 0) - || (j > 0 - && matches[i][j - 1] - && (wildcard[j - 1] == '%' || wildcard[j - 1] == '*')) - || (i > 0 - && j > 0 - && matches[i - 1][j - 1] - && wildcard[j - 1] == name[i - 1] - && wildcard[j - 1] != '%' - && wildcard[j - 1] != '*') - || (i > 0 - && j > 0 - && matches[i - 1][j] - && (wildcard[j - 1] == '*' - || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); - } - } - - matches[name.len()][wildcard.len()] -} - -#[derive(Error, Debug)] -pub enum CommandError { - #[error("Mailbox not found")] - MailboxNotFound, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_wildcard_matches() { - assert!(matches_wildcard("INBOX", "INBOX")); - assert!(matches_wildcard("*", "INBOX")); - assert!(matches_wildcard("%", "INBOX")); - assert!(!matches_wildcard("%", "Test.Azerty")); - assert!(!matches_wildcard("INBOX.*", "INBOX")); - assert!(matches_wildcard("Sent.*", "Sent.A")); - assert!(matches_wildcard("Sent.*", "Sent.A.B")); - assert!(!matches_wildcard("Sent.%", "Sent.A.B")); - } -} diff --git a/aero-proto/imap/command/mod.rs b/aero-proto/imap/command/mod.rs deleted file mode 100644 index f201eb6..0000000 --- a/aero-proto/imap/command/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -pub mod anonymous; -pub mod anystate; -pub mod authenticated; -pub mod selected; - -use crate::mail::namespace::INBOX; -use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; - -/// Convert an IMAP mailbox name/identifier representation -/// to an utf-8 string that is used internally in Aerogramme -struct MailboxName<'a>(&'a MailboxCodec<'a>); -impl<'a> TryInto<&'a str> for MailboxName<'a> { - type Error = std::str::Utf8Error; - fn try_into(self) -> Result<&'a str, Self::Error> { - match self.0 { - MailboxCodec::Inbox => Ok(INBOX), - MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), - } - } -} diff --git a/aero-proto/imap/command/selected.rs b/aero-proto/imap/command/selected.rs deleted file mode 100644 index eedfbd6..0000000 --- a/aero-proto/imap/command/selected.rs +++ /dev/null @@ -1,424 +0,0 @@ -use std::num::NonZeroU64; -use std::sync::Arc; - -use anyhow::Result; -use imap_codec::imap_types::command::{Command, CommandBody, FetchModifier, StoreModifier}; -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; -use imap_codec::imap_types::response::{Code, CodeOther}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anystate, authenticated, MailboxName}; -use crate::imap::flow; -use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; -use crate::imap::response::Response; -use crate::user::User; - -pub struct SelectedContext<'a> { - pub req: &'a Command<'static>, - pub user: &'a Arc, - pub mailbox: &'a mut MailboxView, - pub server_capabilities: &'a ServerCapability, - pub client_capabilities: &'a mut ClientCapability, - pub perm: &'a flow::MailboxPerm, -} - -pub async fn dispatch<'a>( - ctx: SelectedContext<'a>, -) -> Result<(Response<'static>, flow::Transition)> { - match &ctx.req.body { - // Any State - // noop is specific to this state - CommandBody::Capability => { - anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) - } - CommandBody::Logout => anystate::logout(), - - // Specific to this state (7 commands + NOOP) - CommandBody::Close => match ctx.perm { - flow::MailboxPerm::ReadWrite => ctx.close().await, - flow::MailboxPerm::ReadOnly => ctx.examine_close().await, - }, - CommandBody::Noop | CommandBody::Check => ctx.noop().await, - CommandBody::Fetch { - sequence_set, - macro_or_item_names, - modifiers, - uid, - } => { - 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, &SearchKey::And(criteria.clone()), uid) - .await - } - CommandBody::Expunge { - // UIDPLUS (rfc4315) - uid_sequence_set, - } => ctx.expunge(uid_sequence_set).await, - CommandBody::Store { - sequence_set, - kind, - response, - flags, - modifiers, - uid, - } => { - ctx.store(sequence_set, kind, response, flags, modifiers, uid) - .await - } - CommandBody::Copy { - sequence_set, - mailbox, - uid, - } => ctx.copy(sequence_set, mailbox, uid).await, - CommandBody::Move { - sequence_set, - mailbox, - uid, - } => ctx.r#move(sequence_set, mailbox, uid).await, - - // UNSELECT extension (rfc3691) - CommandBody::Unselect => ctx.unselect().await, - - // In selected mode, we fallback to authenticated when needed - _ => { - authenticated::dispatch(authenticated::AuthenticatedContext { - req: ctx.req, - server_capabilities: ctx.server_capabilities, - client_capabilities: ctx.client_capabilities, - user: ctx.user, - }) - .await - } - } -} - -// --- PRIVATE --- - -impl<'a> SelectedContext<'a> { - async fn close(self) -> Result<(Response<'static>, flow::Transition)> { - // We expunge messages, - // but we don't send the untagged EXPUNGE responses - let tag = self.req.tag.clone(); - self.expunge(&None).await?; - Ok(( - Response::build().tag(tag).message("CLOSE completed").ok()?, - flow::Transition::Unselect, - )) - } - - /// CLOSE in examined state is not the same as in selected state - /// (in selected state it also does an EXPUNGE, here it doesn't) - async fn examine_close(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("CLOSE completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { - Ok(( - Response::build() - .to_req(self.req) - .message("UNSELECT completed") - .ok()?, - flow::Transition::Unselect, - )) - } - - pub async fn fetch( - self, - sequence_set: &SequenceSet, - attributes: &'a MacroOrMessageDataItemNames<'static>, - modifiers: &[FetchModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let ap = AttributesProxy::new(attributes, modifiers, *uid); - let mut changed_since: Option = None; - modifiers.iter().for_each(|m| match m { - FetchModifier::ChangedSince(val) => { - changed_since = Some(*val); - } - }); - - match self - .mailbox - .fetch(sequence_set, &ap, changed_since, uid) - .await - { - Ok(resp) => { - // Capabilities enabling logic only on successful command - // (according to my understanding of the spec) - self.client_capabilities.attributes_enable(&ap); - self.client_capabilities.fetch_modifiers_enable(modifiers); - - // Response to the client - Ok(( - Response::build() - .to_req(self.req) - .message("FETCH completed") - .set_body(resp) - .ok()?, - flow::Transition::None, - )) - } - Err(e) => Ok(( - Response::build() - .to_req(self.req) - .message(e.to_string()) - .no()?, - flow::Transition::None, - )), - } - } - - pub async fn search( - self, - charset: &Option>, - criteria: &SearchKey<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; - if enable_condstore { - self.client_capabilities.enable_condstore(); - } - Ok(( - Response::build() - .to_req(self.req) - .set_body(found) - .message("SEARCH completed") - .ok()?, - flow::Transition::None, - )) - } - - pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { - self.mailbox.internal.mailbox.force_sync().await?; - - let updates = self.mailbox.update(UpdateParameters::default()).await?; - Ok(( - Response::build() - .to_req(self.req) - .message("NOOP completed.") - .set_body(updates) - .ok()?, - flow::Transition::None, - )) - } - - async fn expunge( - self, - uid_sequence_set: &Option, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let tag = self.req.tag.clone(); - let data = self.mailbox.expunge(uid_sequence_set).await?; - - Ok(( - Response::build() - .tag(tag) - .message("EXPUNGE completed") - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - async fn store( - self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - modifiers: &[StoreModifier], - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let mut unchanged_since: Option = None; - modifiers.iter().for_each(|m| match m { - StoreModifier::UnchangedSince(val) => { - unchanged_since = Some(*val); - } - }); - - let (data, modified) = self - .mailbox - .store(sequence_set, kind, response, flags, unchanged_since, uid) - .await?; - - let mut ok_resp = Response::build() - .to_req(self.req) - .message("STORE completed") - .set_body(data); - - match modified[..] { - [] => (), - [_head, ..] => { - let modified_str = format!( - "MODIFIED {}", - modified - .into_iter() - .map(|x| x.to_string()) - .collect::>() - .join(",") - ); - ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated( - modified_str.into_bytes(), - ))); - } - }; - - self.client_capabilities.store_modifiers_enable(modifiers); - - Ok((ok_resp.ok()?, flow::Transition::None)) - } - - async fn copy( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - //@FIXME Could copy be valid in EXAMINE mode? - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map) = self.mailbox.copy(sequence_set, mb, uid).await?; - - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .ok()?, - flow::Transition::None, - )) - } - - async fn r#move( - self, - sequence_set: &SequenceSet, - mailbox: &MailboxCodec<'a>, - uid: &bool, - ) -> Result<(Response<'static>, flow::Transition)> { - if let Some(failed) = self.fail_read_only() { - return Ok((failed, flow::Transition::None)); - } - - let name: &str = MailboxName(mailbox).try_into()?; - - let mb_opt = self.user.open_mailbox(&name).await?; - let mb = match mb_opt { - Some(mb) => mb, - None => { - return Ok(( - Response::build() - .to_req(self.req) - .message("Destination mailbox does not exist") - .code(Code::TryCreate) - .no()?, - flow::Transition::None, - )) - } - }; - - let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; - - // compute code - let copyuid_str = format!( - "{} {} {}", - uidval, - uid_map - .iter() - .map(|(sid, _)| format!("{}", sid)) - .collect::>() - .join(","), - uid_map - .iter() - .map(|(_, tuid)| format!("{}", tuid)) - .collect::>() - .join(",") - ); - - Ok(( - Response::build() - .to_req(self.req) - .message("COPY completed") - .code(Code::Other(CodeOther::unvalidated( - format!("COPYUID {}", copyuid_str).into_bytes(), - ))) - .set_body(data) - .ok()?, - flow::Transition::None, - )) - } - - fn fail_read_only(&self) -> Option> { - match self.perm { - flow::MailboxPerm::ReadWrite => None, - flow::MailboxPerm::ReadOnly => Some( - Response::build() - .to_req(self.req) - .message("Write command are forbidden while exmining mailbox") - .no() - .unwrap(), - ), - } - } -} diff --git a/aero-proto/imap/flags.rs b/aero-proto/imap/flags.rs deleted file mode 100644 index 0f6ec64..0000000 --- a/aero-proto/imap/flags.rs +++ /dev/null @@ -1,30 +0,0 @@ -use imap_codec::imap_types::core::Atom; -use imap_codec::imap_types::flag::{Flag, FlagFetch}; - -pub fn from_str(f: &str) -> Option> { - match f.chars().next() { - Some('\\') => match f { - "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)), - "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)), - "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)), - "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)), - "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)), - "\\Recent" => Some(FlagFetch::Recent), - _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), - }, - }, - Some(_) => match Atom::try_from(f.to_string()) { - Err(_) => { - tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); - None - } - Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), - }, - None => None, - } -} diff --git a/aero-proto/imap/flow.rs b/aero-proto/imap/flow.rs deleted file mode 100644 index 86eb12e..0000000 --- a/aero-proto/imap/flow.rs +++ /dev/null @@ -1,114 +0,0 @@ -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::user::User; - -#[derive(Debug)] -pub enum Error { - ForbiddenTransition, -} -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Forbidden Transition") - } -} -impl StdError for Error {} - -pub enum State { - NotAuthenticated, - Authenticated(Arc), - Selected(Arc, MailboxView, MailboxPerm), - Idle( - Arc, - MailboxView, - MailboxPerm, - Tag<'static>, - Arc, - ), - Logout, -} -impl State { - pub fn notify(&self) -> Option> { - 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::*; - match self { - NotAuthenticated => write!(f, "NotAuthenticated"), - Authenticated(..) => write!(f, "Authenticated"), - Selected(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - Logout => write!(f, "Logout"), - } - } -} - -#[derive(Clone)] -pub enum MailboxPerm { - ReadOnly, - ReadWrite, -} - -pub enum Transition { - None, - Authenticate(Arc), - Select(MailboxView, MailboxPerm), - Idle(Tag<'static>, Notify), - UnIdle, - Unselect, - Logout, -} -impl fmt::Display for Transition { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use Transition::*; - match self { - None => write!(f, "None"), - Authenticate(..) => write!(f, "Authenticated"), - Select(..) => write!(f, "Selected"), - Idle(..) => write!(f, "Idle"), - UnIdle => write!(f, "UnIdle"), - Unselect => write!(f, "Unselect"), - Logout => write!(f, "Logout"), - } - } -} - -// See RFC3501 section 3. -// https://datatracker.ietf.org/doc/html/rfc3501#page-13 -impl State { - pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { - tracing::debug!(state=%self, transition=%tr, "try change state"); - - let new_state = match (std::mem::replace(self, State::Logout), tr) { - (s, Transition::None) => s, - (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), - (State::Authenticated(u) | State::Selected(u, _, _), Transition::Select(m, p)) => { - State::Selected(u, m, p) - } - (State::Selected(u, _, _), Transition::Unselect) => State::Authenticated(u.clone()), - (State::Selected(u, m, p), Transition::Idle(t, s)) => { - State::Idle(u, m, p, t, Arc::new(s)) - } - (State::Idle(u, m, p, _, _), Transition::UnIdle) => State::Selected(u, m, p), - (_, Transition::Logout) => State::Logout, - (s, t) => { - tracing::error!(state=%s, transition=%t, "forbidden transition"); - return Err(Error::ForbiddenTransition); - } - }; - *self = new_state; - tracing::debug!(state=%self, "transition succeeded"); - - Ok(()) - } -} diff --git a/aero-proto/imap/imf_view.rs b/aero-proto/imap/imf_view.rs deleted file mode 100644 index a4ca2e8..0000000 --- a/aero-proto/imap/imf_view.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anyhow::{anyhow, Result}; -use chrono::naive::NaiveDate; - -use imap_codec::imap_types::core::{IString, NString}; -use imap_codec::imap_types::envelope::{Address, Envelope}; - -use eml_codec::imf; - -pub struct ImfView<'a>(pub &'a imf::Imf<'a>); - -impl<'a> ImfView<'a> { - pub fn naive_date(&self) -> Result { - Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive()) - } - - /// Envelope rules are defined in RFC 3501, section 7.4.2 - /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 - /// - /// Some important notes: - /// - /// If the Sender or Reply-To lines are absent in the [RFC-2822] - /// header, or are present but empty, the server sets the - /// corresponding member of the envelope to be the same value as - /// the from member (the client is not expected to know to do - /// this). Note: [RFC-2822] requires that all messages have a valid - /// From header. Therefore, the from, sender, and reply-to - /// members in the envelope can not be NIL. - /// - /// If the Date, Subject, In-Reply-To, and Message-ID header lines - /// are absent in the [RFC-2822] header, the corresponding member - /// of the envelope is NIL; if these header lines are present but - /// empty the corresponding member of the envelope is the empty - /// string. - - //@FIXME return an error if the envelope is invalid instead of panicking - //@FIXME some fields must be defaulted if there are not set. - pub fn message_envelope(&self) -> Envelope<'static> { - let msg = self.0; - let from = msg.from.iter().map(convert_mbx).collect::>(); - - Envelope { - date: NString( - msg.date - .as_ref() - .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), - ), - subject: NString( - msg.subject - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - sender: msg - .sender - .as_ref() - .map(|v| vec![convert_mbx(v)]) - .unwrap_or(from.clone()), - reply_to: if msg.reply_to.is_empty() { - from.clone() - } else { - convert_addresses(&msg.reply_to) - }, - from, - to: convert_addresses(&msg.to), - cc: convert_addresses(&msg.cc), - bcc: convert_addresses(&msg.bcc), - in_reply_to: NString( - msg.in_reply_to - .iter() - .next() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - message_id: NString( - msg.msg_id - .as_ref() - .map(|d| IString::try_from(d.to_string()).unwrap()), - ), - } - } -} - -pub fn convert_addresses(addrlist: &Vec) -> Vec> { - let mut acc = vec![]; - for item in addrlist { - match item { - imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)), - imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx)), - } - } - return acc; -} - -pub fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> { - Address { - name: NString( - addr.name - .as_ref() - .map(|x| IString::try_from(x.to_string()).unwrap()), - ), - // SMTP at-domain-list (source route) seems obsolete since at least 1991 - // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html - adl: NString(None), - mailbox: NString(Some( - IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), - )), - host: NString(Some( - IString::try_from(addr.addrspec.domain.to_string()).unwrap(), - )), - } -} diff --git a/aero-proto/imap/index.rs b/aero-proto/imap/index.rs deleted file mode 100644 index 9b794b8..0000000 --- a/aero-proto/imap/index.rs +++ /dev/null @@ -1,211 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -use anyhow::{anyhow, Result}; -use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; - -use crate::mail::uidindex::{ImapUid, ModSeq, UidIndex}; -use crate::mail::unique_ident::UniqueIdent; - -pub struct Index<'a> { - pub imap_index: Vec>, - pub internal: &'a UidIndex, -} -impl<'a> Index<'a> { - pub fn new(internal: &'a UidIndex) -> Result { - let imap_index = internal - .idx_by_uid - .iter() - .enumerate() - .map(|(i_enum, (&uid, &uuid))| { - let (_, modseq, flags) = internal - .table - .get(&uuid) - .ok_or(anyhow!("mail is missing from index"))?; - let i_int: u32 = (i_enum + 1).try_into()?; - let i: NonZeroU32 = i_int.try_into()?; - - Ok(MailIndex { - i, - uid, - uuid, - modseq: *modseq, - flags, - }) - }) - .collect::>>()?; - - Ok(Self { - imap_index, - internal, - }) - } - - pub fn last(&'a self) -> Option<&'a MailIndex<'a>> { - self.imap_index.last() - } - - /// Fetch mail descriptors based on a sequence of UID - /// - /// Complexity analysis: - /// - Sort is O(n * log n) where n is the number of uid generated by the sequence - /// - Finding the starting point in the index O(log m) where m is the size of the mailbox - /// While n =< m, it's not clear if the difference is big or not. - /// - /// For now, the algorithm tries to be fast for small values of n, - /// as it is what is expected by clients. - /// - /// So we assume for our implementation that : n << m. - /// It's not true for full mailbox searches for example... - pub fn fetch_on_uid(&'a self, sequence_set: &SequenceSet) -> Vec<&'a MailIndex<'a>> { - if self.imap_index.is_empty() { - return vec![]; - } - let largest = self.last().expect("The mailbox is not empty").uid; - let mut unroll_seq = sequence_set.iter(largest).collect::>(); - unroll_seq.sort(); - - let start_seq = match unroll_seq.iter().next() { - Some(elem) => elem, - None => return vec![], - }; - - // Quickly jump to the right point in the mailbox vector O(log m) instead - // of iterating one by one O(m). Works only because both unroll_seq & imap_index are sorted per uid. - let mut imap_idx = { - let start_idx = self - .imap_index - .partition_point(|mail_idx| &mail_idx.uid < start_seq); - &self.imap_index[start_idx..] - }; - - let mut acc = vec![]; - for wanted_uid in unroll_seq.iter() { - // Slide the window forward as long as its first element is lower than our wanted uid. - let start_idx = match imap_idx.iter().position(|midx| &midx.uid >= wanted_uid) { - Some(v) => v, - None => break, - }; - imap_idx = &imap_idx[start_idx..]; - - // If the beginning of our new window is the uid we want, we collect it - if &imap_idx[0].uid == wanted_uid { - acc.push(&imap_idx[0]); - } - } - - acc - } - - pub fn fetch_on_id(&'a self, sequence_set: &SequenceSet) -> Result>> { - if self.imap_index.is_empty() { - return Ok(vec![]); - } - let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?; - let mut acc = sequence_set - .iter(largest) - .map(|wanted_id| { - self.imap_index - .get((wanted_id.get() as usize) - 1) - .ok_or(anyhow!("Mail not found")) - }) - .collect::>>()?; - - // Sort the result to be consistent with UID - acc.sort_by(|a, b| a.i.cmp(&b.i)); - - Ok(acc) - } - - pub fn fetch( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - by_uid: bool, - ) -> Result>> { - match by_uid { - true => Ok(self.fetch_on_uid(sequence_set)), - _ => self.fetch_on_id(sequence_set), - } - } - - pub fn fetch_changed_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option, - by_uid: bool, - ) -> Result>> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(), - None => raw, - }; - - Ok(res) - } - - pub fn fetch_unchanged_since( - self: &'a Index<'a>, - sequence_set: &SequenceSet, - maybe_modseq: Option, - by_uid: bool, - ) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> { - let raw = self.fetch(sequence_set, by_uid)?; - let res = match maybe_modseq { - Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit), - None => (raw, vec![]), - }; - - Ok(res) - } -} - -#[derive(Clone, Debug)] -pub struct MailIndex<'a> { - pub i: NonZeroU32, - pub uid: ImapUid, - pub uuid: UniqueIdent, - pub modseq: ModSeq, - pub flags: &'a Vec, -} - -impl<'a> MailIndex<'a> { - // The following functions are used to implement the SEARCH command - pub fn is_in_sequence_i(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.i, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.i, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.i && &self.i <= x2 - } else { - x1 >= &self.i && &self.i >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_in_sequence_uid(&self, seq: &Sequence) -> bool { - match seq { - Sequence::Single(SeqOrUid::Asterisk) => true, - Sequence::Single(SeqOrUid::Value(target)) => target == &self.uid, - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) - | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.uid, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - if x1 < x2 { - x1 <= &self.uid && &self.uid <= x2 - } else { - x1 >= &self.uid && &self.uid >= x2 - } - } - Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, - } - } - - pub fn is_flag_set(&self, flag: &str) -> bool { - self.flags - .iter() - .any(|candidate| candidate.as_str() == flag) - } -} diff --git a/aero-proto/imap/mail_view.rs b/aero-proto/imap/mail_view.rs deleted file mode 100644 index a8db733..0000000 --- a/aero-proto/imap/mail_view.rs +++ /dev/null @@ -1,306 +0,0 @@ -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; -use chrono::{naive::NaiveDate, DateTime as ChronoDateTime, Local, Offset, TimeZone, Utc}; - -use imap_codec::imap_types::core::NString; -use imap_codec::imap_types::datetime::DateTime; -use imap_codec::imap_types::fetch::{ - MessageDataItem, MessageDataItemName, Section as FetchSection, -}; -use imap_codec::imap_types::flag::Flag; -use imap_codec::imap_types::response::Data; - -use eml_codec::{ - imf, - part::{composite::Message, AnyPart}, -}; - -use crate::mail::query::QueryResult; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::imf_view::ImfView; -use crate::imap::index::MailIndex; -use crate::imap::mime_view; -use crate::imap::response::Body; - -pub struct MailView<'a> { - pub in_idx: &'a MailIndex<'a>, - pub query_result: &'a QueryResult, - pub content: FetchedMail<'a>, -} - -impl<'a> MailView<'a> { - pub fn new(query_result: &'a QueryResult, in_idx: &'a MailIndex<'a>) -> Result> { - Ok(Self { - in_idx, - query_result, - content: match query_result { - QueryResult::FullResult { content, .. } => { - let (_, parsed) = - eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?; - FetchedMail::full_from_message(parsed) - } - QueryResult::PartialResult { metadata, .. } => { - let (_, parsed) = eml_codec::parse_message(&metadata.headers) - .or(Err(anyhow!("unable to parse email headers")))?; - FetchedMail::partial_from_message(parsed) - } - QueryResult::IndexResult { .. } => FetchedMail::IndexOnly, - }, - }) - } - - pub fn imf(&self) -> Option { - self.content.as_imf().map(ImfView) - } - - pub fn selected_mime(&'a self) -> Option> { - self.content.as_anypart().ok().map(mime_view::SelectedMime) - } - - pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { - let mut seen = SeenFlag::DoNothing; - let res_attrs = ap - .attrs - .iter() - .map(|attr| match attr { - MessageDataItemName::Uid => Ok(self.uid()), - MessageDataItemName::Flags => Ok(self.flags()), - MessageDataItemName::Rfc822Size => self.rfc_822_size(), - MessageDataItemName::Rfc822Header => self.rfc_822_header(), - MessageDataItemName::Rfc822Text => self.rfc_822_text(), - MessageDataItemName::Rfc822 => { - if self.is_not_yet_seen() { - seen = SeenFlag::MustAdd; - } - self.rfc822() - } - MessageDataItemName::Envelope => Ok(self.envelope()), - MessageDataItemName::Body => self.body(), - MessageDataItemName::BodyStructure => self.body_structure(), - MessageDataItemName::BodyExt { - section, - partial, - peek, - } => { - let (body, has_seen) = self.body_ext(section, partial, peek)?; - seen = has_seen; - Ok(body) - } - MessageDataItemName::InternalDate => self.internal_date(), - MessageDataItemName::ModSeq => Ok(self.modseq()), - }) - .collect::, _>>()?; - - Ok(( - Body::Data(Data::Fetch { - seq: self.in_idx.i, - items: res_attrs.try_into()?, - }), - seen, - )) - } - - pub fn stored_naive_date(&self) -> Result { - let mail_meta = self.query_result.metadata().expect("metadata were fetched"); - let mail_ts: i64 = mail_meta.internaldate.try_into()?; - let msg_date: ChronoDateTime = ChronoDateTime::from_timestamp(mail_ts, 0) - .ok_or(anyhow!("unable to parse timestamp"))? - .with_timezone(&Local); - - Ok(msg_date.date_naive()) - } - - pub fn is_header_contains_pattern(&self, hdr: &[u8], pattern: &[u8]) -> bool { - let mime = match self.selected_mime() { - None => return false, - Some(x) => x, - }; - - let val = match mime.header_value(hdr) { - None => return false, - Some(x) => x, - }; - - val.windows(pattern.len()).any(|win| win == pattern) - } - - // Private function, mainly for filter! - fn uid(&self) -> MessageDataItem<'static> { - MessageDataItem::Uid(self.in_idx.uid.clone()) - } - - fn flags(&self) -> MessageDataItem<'static> { - MessageDataItem::Flags( - self.in_idx - .flags - .iter() - .filter_map(|f| flags::from_str(f)) - .collect(), - ) - } - - fn rfc_822_size(&self) -> Result> { - let sz = self - .query_result - .metadata() - .ok_or(anyhow!("mail metadata are required"))? - .rfc822_size; - Ok(MessageDataItem::Rfc822Size(sz as u32)) - } - - fn rfc_822_header(&self) -> Result> { - let hdrs: NString = self - .query_result - .metadata() - .ok_or(anyhow!("mail metadata are required"))? - .headers - .to_vec() - .try_into()?; - Ok(MessageDataItem::Rfc822Header(hdrs)) - } - - fn rfc_822_text(&self) -> Result> { - let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822Text(txt)) - } - - fn rfc822(&self) -> Result> { - let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?; - Ok(MessageDataItem::Rfc822(full)) - } - - fn envelope(&self) -> MessageDataItem<'static> { - MessageDataItem::Envelope( - self.imf() - .expect("an imf object is derivable from fetchedmail") - .message_envelope(), - ) - } - - fn body(&self) -> Result> { - Ok(MessageDataItem::Body(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - false, - )?)) - } - - fn body_structure(&self) -> Result> { - Ok(MessageDataItem::BodyStructure(mime_view::bodystructure( - self.content.as_msg()?.child.as_ref(), - true, - )?)) - } - - fn is_not_yet_seen(&self) -> bool { - let seen_flag = Flag::Seen.to_string(); - !self.in_idx.flags.iter().any(|x| *x == seen_flag) - } - - /// maps to BODY[
]<> and BODY.PEEK[
]<> - /// peek does not implicitly set the \Seen flag - /// eg. BODY[HEADER.FIELDS (DATE FROM)] - /// eg. BODY[]<0.2048> - fn body_ext( - &self, - section: &Option>, - partial: &Option<(u32, NonZeroU32)>, - peek: &bool, - ) -> Result<(MessageDataItem<'static>, SeenFlag)> { - // Manage Seen flag - let mut seen = SeenFlag::DoNothing; - if !peek && self.is_not_yet_seen() { - // Add \Seen flag - //self.mailbox.add_flags(uuid, &[seen_flag]).await?; - seen = SeenFlag::MustAdd; - } - - // Process message - let (text, origin) = - match mime_view::body_ext(self.content.as_anypart()?, section, partial)? { - mime_view::BodySection::Full(body) => (body, None), - mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)), - }; - - let data: NString = text.to_vec().try_into()?; - - return Ok(( - MessageDataItem::BodyExt { - section: section.as_ref().map(|fs| fs.clone()), - origin, - data, - }, - seen, - )); - } - - fn internal_date(&self) -> Result> { - let dt = Utc - .fix() - .timestamp_opt( - i64::try_from( - self.query_result - .metadata() - .ok_or(anyhow!("mail metadata were not fetched"))? - .internaldate - / 1000, - )?, - 0, - ) - .earliest() - .ok_or(anyhow!("Unable to parse internal date"))?; - Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) - } - - fn modseq(&self) -> MessageDataItem<'static> { - MessageDataItem::ModSeq(self.in_idx.modseq) - } -} - -pub enum SeenFlag { - DoNothing, - MustAdd, -} - -// ------------------- - -pub enum FetchedMail<'a> { - IndexOnly, - Partial(AnyPart<'a>), - Full(AnyPart<'a>), -} -impl<'a> FetchedMail<'a> { - pub fn full_from_message(msg: Message<'a>) -> Self { - Self::Full(AnyPart::Msg(msg)) - } - - pub fn partial_from_message(msg: Message<'a>) -> Self { - Self::Partial(AnyPart::Msg(msg)) - } - - pub fn as_anypart(&self) -> Result<&AnyPart<'a>> { - match self { - FetchedMail::Full(x) => Ok(&x), - FetchedMail::Partial(x) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers"), - } - } - - pub fn as_msg(&self) -> Result<&Message<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x), - FetchedMail::Partial(AnyPart::Msg(x)) => Ok(&x), - _ => bail!("The full message must be fetched, not only its headers AND it must be an AnyPart::Msg."), - } - } - - pub fn as_imf(&self) -> Option<&imf::Imf<'a>> { - match self { - FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), - FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf), - _ => None, - } - } -} diff --git a/aero-proto/imap/mailbox_view.rs b/aero-proto/imap/mailbox_view.rs deleted file mode 100644 index 1c53b93..0000000 --- a/aero-proto/imap/mailbox_view.rs +++ /dev/null @@ -1,772 +0,0 @@ -use std::collections::HashSet; -use std::num::{NonZeroU32, NonZeroU64}; -use std::sync::Arc; - -use anyhow::{anyhow, Error, Result}; - -use futures::stream::{StreamExt, TryStreamExt}; - -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}; -use imap_codec::imap_types::search::SearchKey; -use imap_codec::imap_types::sequence::SequenceSet; - -use crate::mail::mailbox::Mailbox; -use crate::mail::query::QueryScope; -use crate::mail::snapshot::FrozenMailbox; -use crate::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq}; -use crate::mail::unique_ident::UniqueIdent; - -use crate::imap::attributes::AttributesProxy; -use crate::imap::flags; -use crate::imap::index::Index; -use crate::imap::mail_view::{MailView, SeenFlag}; -use crate::imap::response::Body; -use crate::imap::search; - -const DEFAULT_FLAGS: [Flag; 5] = [ - Flag::Seen, - Flag::Answered, - Flag::Flagged, - Flag::Deleted, - Flag::Draft, -]; - -pub struct UpdateParameters { - pub silence: HashSet, - pub with_modseq: bool, - pub with_uid: bool, -} -impl Default for UpdateParameters { - fn default() -> Self { - Self { - silence: HashSet::new(), - with_modseq: false, - with_uid: false, - } - } -} - -/// A MailboxView is responsible for giving the client the information -/// it needs about a mailbox, such as an initial summary of the mailbox's -/// content and continuous updates indicating when the content -/// of the mailbox has been changed. -/// To do this, it keeps a variable `known_state` that corresponds to -/// what the client knows, and produces IMAP messages to be sent to the -/// client that go along updates to `known_state`. -pub struct MailboxView { - pub internal: FrozenMailbox, - pub is_condstore: bool, -} - -impl MailboxView { - /// Creates a new IMAP view into a mailbox. - pub async fn new(mailbox: Arc, is_cond: bool) -> Self { - Self { - internal: mailbox.frozen().await, - is_condstore: is_cond, - } - } - - /// Create an updated view, useful to make a diff - /// between what the client knows and new stuff - /// Produces a set of IMAP responses describing the change between - /// what the client knows and what is actually in the mailbox. - /// This does NOT trigger a sync, it bases itself on what is currently - /// loaded in RAM by Bayou. - pub async fn update(&mut self, params: UpdateParameters) -> Result>> { - let old_snapshot = self.internal.update().await; - let new_snapshot = &self.internal.snapshot; - - let mut data = Vec::::new(); - - // Calculate diff between two mailbox states - // See example in IMAP RFC in section on NOOP command: - // we want to produce something like this: - // C: a047 NOOP - // S: * 22 EXPUNGE - // S: * 23 EXISTS - // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) - // S: a047 OK Noop completed - // In other words: - // - notify client of expunged mails - // - if new mails arrived, notify client of number of existing mails - // - if flags changed for existing mails, tell client - // (for this last step: if uidvalidity changed, do nothing, - // just notify of new uidvalidity and they will resync) - - // - notify client of expunged mails - let mut n_expunge = 0; - for (i, (_uid, uuid)) in old_snapshot.idx_by_uid.iter().enumerate() { - if !new_snapshot.table.contains_key(uuid) { - data.push(Body::Data(Data::Expunge( - NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), - ))); - n_expunge += 1; - } - } - - // - if new mails arrived, notify client of number of existing mails - if new_snapshot.table.len() != old_snapshot.table.len() - n_expunge - || new_snapshot.uidvalidity != old_snapshot.uidvalidity - { - data.push(self.exists_status()?); - } - - if new_snapshot.uidvalidity != old_snapshot.uidvalidity { - // TODO: do we want to push less/more info than this? - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - } else { - // - if flags changed for existing mails, tell client - for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { - if params.silence.contains(uuid) { - continue; - } - - let old_mail = old_snapshot.table.get(uuid); - let new_mail = new_snapshot.table.get(uuid); - if old_mail.is_some() && old_mail != new_mail { - if let Some((uid, modseq, flags)) = new_mail { - let mut items = vec![MessageDataItem::Flags( - flags.iter().filter_map(|f| flags::from_str(f)).collect(), - )]; - - if params.with_uid { - items.push(MessageDataItem::Uid(*uid)); - } - - if params.with_modseq { - items.push(MessageDataItem::ModSeq(*modseq)); - } - - data.push(Body::Data(Data::Fetch { - seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), - items: items.try_into()?, - })); - } - } - } - } - Ok(data) - } - - /// Generates the necessary IMAP messages so that the client - /// has a satisfactory summary of the current mailbox's state. - /// These are the messages that are sent in response to a SELECT command. - pub fn summary(&self) -> Result>> { - let mut data = Vec::::new(); - data.push(self.exists_status()?); - data.push(self.recent_status()?); - data.extend(self.flags_status()?.into_iter()); - data.push(self.uidvalidity_status()?); - data.push(self.uidnext_status()?); - if self.is_condstore { - data.push(self.highestmodseq_status()?); - } - /*self.unseen_first_status()? - .map(|unseen_status| data.push(unseen_status));*/ - - Ok(data) - } - - pub async fn store<'a>( - &mut self, - sequence_set: &SequenceSet, - kind: &StoreType, - response: &StoreResponse, - flags: &[Flag<'a>], - unchanged_since: Option, - is_uid_store: &bool, - ) -> Result<(Vec>, Vec)> { - self.internal.sync().await?; - - let flags = flags.iter().map(|x| x.to_string()).collect::>(); - - let idx = self.index()?; - let (editable, in_conflict) = - idx.fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?; - - for mi in editable.iter() { - match kind { - StoreType::Add => { - self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Remove => { - self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?; - } - StoreType::Replace => { - self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?; - } - } - } - - let silence = match response { - StoreResponse::Answer => HashSet::new(), - StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(), - }; - - let conflict_id_or_uid = match is_uid_store { - true => in_conflict.into_iter().map(|midx| midx.uid).collect(), - _ => in_conflict.into_iter().map(|midx| midx.i).collect(), - }; - - let summary = self - .update(UpdateParameters { - with_uid: *is_uid_store, - with_modseq: unchanged_since.is_some(), - silence, - }) - .await?; - - Ok((summary, conflict_id_or_uid)) - } - - pub async fn idle_sync(&mut self) -> Result>> { - self.internal - .mailbox - .notify() - .await - .upgrade() - .ok_or(anyhow!("test"))? - .notified() - .await; - self.internal.mailbox.opportunistic_sync().await?; - self.update(UpdateParameters::default()).await - } - - pub async fn expunge( - &mut self, - maybe_seq_set: &Option, - ) -> Result>> { - // Get a recent view to apply our change - self.internal.sync().await?; - let state = self.internal.peek().await; - let idx = Index::new(&state)?; - - // Build a default sequence set for the default case - use imap_codec::imap_types::sequence::{SeqOrUid, Sequence}; - let seq = match maybe_seq_set { - Some(s) => s.clone(), - None => SequenceSet( - vec![Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )] - .try_into() - .unwrap(), - ), - }; - - let deleted_flag = Flag::Deleted.to_string(); - let msgs = idx - .fetch_on_uid(&seq) - .into_iter() - .filter(|midx| midx.flags.iter().any(|x| *x == deleted_flag)) - .map(|midx| midx.uuid); - - for msg in msgs { - self.internal.mailbox.delete(msg).await?; - } - - self.update(UpdateParameters::default()).await - } - - pub async fn copy( - &self, - sequence_set: &SequenceSet, - to: Arc, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - let mut new_uuids = vec![]; - for mi in mails.iter() { - new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?); - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { - let dest_uid = to_state - .table - .get(new_uuid) - .ok_or(anyhow!("copied mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - Ok((to_state.uidvalidity, ret)) - } - - pub async fn r#move( - &mut self, - sequence_set: &SequenceSet, - to: Arc, - is_uid_copy: &bool, - ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec>)> { - let idx = self.index()?; - let mails = idx.fetch(sequence_set, *is_uid_copy)?; - - for mi in mails.iter() { - to.move_from(&self.internal.mailbox, mi.uuid).await?; - } - - let mut ret = vec![]; - let to_state = to.current_uid_index().await; - for mi in mails.iter() { - let dest_uid = to_state - .table - .get(&mi.uuid) - .ok_or(anyhow!("moved mail not in destination mailbox"))? - .0; - ret.push((mi.uid, dest_uid)); - } - - let update = self - .update(UpdateParameters { - with_uid: *is_uid_copy, - ..UpdateParameters::default() - }) - .await?; - - Ok((to_state.uidvalidity, ret, update)) - } - - /// Looks up state changes in the mailbox and produces a set of IMAP - /// responses describing the new state. - pub async fn fetch<'b>( - &self, - sequence_set: &SequenceSet, - ap: &AttributesProxy, - changed_since: Option, - is_uid_fetch: &bool, - ) -> Result>> { - // [1/6] Pre-compute data - // a. what are the uuids of the emails we want? - // b. do we need to fetch the full body? - //let ap = AttributesProxy::new(attributes, *is_uid_fetch); - let query_scope = match ap.need_body() { - true => QueryScope::Full, - _ => QueryScope::Partial, - }; - tracing::debug!("Query scope {:?}", query_scope); - let idx = self.index()?; - let mail_idx_list = idx.fetch_changed_since(sequence_set, changed_since, *is_uid_fetch)?; - - // [2/6] Fetch the emails - let uuids = mail_idx_list - .iter() - .map(|midx| midx.uuid) - .collect::>(); - - let query = self.internal.query(&uuids, query_scope); - //let query_result = self.internal.query(&uuids, query_scope).fetch().await?; - - 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), - }) - // [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. - query_stream.try_collect().await - } - - /// A naive search implementation... - pub async fn search<'a>( - &self, - _charset: &Option>, - search_key: &SearchKey<'a>, - uid: bool, - ) -> Result<(Vec>, bool)> { - // 1. Compute the subset of sequence identifiers we need to fetch - // based on the search query - let crit = search::Criteria(search_key); - let (seq_set, seq_type) = crit.to_sequence_set(); - - // 2. Get the selection - let idx = self.index()?; - let selection = idx.fetch(&seq_set, seq_type.is_uid())?; - - // 3. Filter the selection based on the ID / UID / Flags - let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); - - // 4.a Fetch additional info about the emails - let query_scope = crit.query_scope(); - let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::>(); - let query = self.internal.query(&uuids, query_scope); - - // 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) - }); - - // 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), - }); - - // 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, maybe_modseq))], - maybe_modseq.is_some(), - )) - } - - // ---- - /// @FIXME index should be stored for longer than a single request - /// Instead they should be tied to the FrozenMailbox refresh - /// It's not trivial to refactor the code to do that, so we are doing - /// some useless computation for now... - fn index<'a>(&'a self) -> Result> { - Index::new(&self.internal.snapshot) - } - - /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` - fn uidvalidity_status(&self) -> Result> { - let uid_validity = Status::ok( - None, - Some(Code::UidValidity(self.uidvalidity())), - "UIDs valid", - ) - .map_err(Error::msg)?; - Ok(Body::Status(uid_validity)) - } - - pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { - self.internal.snapshot.uidvalidity - } - - /// Produce an OK [UIDNEXT _] message corresponding to `known_state` - fn uidnext_status(&self) -> Result> { - let next_uid = Status::ok( - None, - Some(Code::UidNext(self.uidnext())), - "Predict next UID", - ) - .map_err(Error::msg)?; - Ok(Body::Status(next_uid)) - } - - pub(crate) fn uidnext(&self) -> ImapUid { - self.internal.snapshot.uidnext - } - - pub(crate) fn highestmodseq_status(&self) -> Result> { - Ok(Body::Status(Status::ok( - None, - Some(Code::Other(CodeOther::unvalidated( - format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes(), - ))), - "Highest", - )?)) - } - - pub(crate) fn highestmodseq(&self) -> ModSeq { - self.internal.snapshot.highestmodseq - } - - /// Produce an EXISTS message corresponding to the number of mails - /// in `known_state` - fn exists_status(&self) -> Result> { - Ok(Body::Data(Data::Exists(self.exists()?))) - } - - pub(crate) fn exists(&self) -> Result { - Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?) - } - - /// Produce a RECENT message corresponding to the number of - /// recent mails in `known_state` - fn recent_status(&self) -> Result> { - Ok(Body::Data(Data::Recent(self.recent()?))) - } - - #[allow(dead_code)] - fn unseen_first_status(&self) -> Result>> { - Ok(self - .unseen_first()? - .map(|unseen_id| { - Status::ok(None, Some(Code::Unseen(unseen_id)), "First unseen.").map(Body::Status) - }) - .transpose()?) - } - - #[allow(dead_code)] - fn unseen_first(&self) -> Result> { - Ok(self - .internal - .snapshot - .table - .values() - .enumerate() - .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string())) - .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) - .transpose()?) - } - - pub(crate) fn recent(&self) -> Result { - let recent = self - .internal - .snapshot - .idx_by_flag - .get(&"\\Recent".to_string()) - .map(|os| os.len()) - .unwrap_or(0); - Ok(u32::try_from(recent)?) - } - - /// Produce a FLAGS and a PERMANENTFLAGS message that indicates - /// the flags that are in `known_state` + default flags - fn flags_status(&self) -> Result>> { - let mut body = vec![]; - - // 1. Collecting all the possible flags in the mailbox - // 1.a Fetch them from our index - let mut known_flags: Vec = self - .internal - .snapshot - .idx_by_flag - .flags() - .filter_map(|f| match flags::from_str(f) { - Some(FlagFetch::Flag(fl)) => Some(fl), - _ => None, - }) - .collect(); - // 1.b Merge it with our default flags list - for f in DEFAULT_FLAGS.iter() { - if !known_flags.contains(f) { - known_flags.push(f.clone()); - } - } - // 1.c Create the IMAP message - body.push(Body::Data(Data::Flags(known_flags.clone()))); - - // 2. Returning flags that are persisted - // 2.a Always advertise our default flags - let mut permanent = DEFAULT_FLAGS - .iter() - .map(|f| FlagPerm::Flag(f.clone())) - .collect::>(); - // 2.b Say that we support any keyword flag - permanent.push(FlagPerm::Asterisk); - // 2.c Create the IMAP message - let permanent_flags = Status::ok( - None, - Some(Code::PermanentFlags(permanent)), - "Flags permitted", - ) - .map_err(Error::msg)?; - body.push(Body::Status(permanent_flags)); - - // Done! - Ok(body) - } - - pub(crate) fn unseen_count(&self) -> usize { - let total = self.internal.snapshot.table.len(); - let seen = self - .internal - .snapshot - .idx_by_flag - .get(&Flag::Seen.to_string()) - .map(|x| x.len()) - .unwrap_or(0); - total - seen - } -} - -#[cfg(test)] -mod tests { - use super::*; - use imap_codec::encode::Encoder; - 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; - use imap_codec::ResponseCodec; - use std::fs; - - use crate::cryptoblob; - use crate::imap::index::MailIndex; - use crate::imap::mail_view::MailView; - use crate::imap::mime_view; - use crate::mail::mailbox::MailMeta; - use crate::mail::query::QueryResult; - use crate::mail::unique_ident; - - #[test] - fn mailview_body_ext() -> Result<()> { - let ap = AttributesProxy::new( - &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ - MessageDataItemName::BodyExt { - section: Some(Section::Header(None)), - partial: None, - peek: false, - }, - ]), - &[], - false, - ); - - let key = cryptoblob::gen_key(); - let meta = MailMeta { - internaldate: 0u64, - headers: vec![], - message_key: key, - rfc822_size: 8usize, - }; - - let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]); - let mail_in_idx = MailIndex { - i: NonZeroU32::MIN, - uid: index_entry.0, - modseq: index_entry.1, - uuid: unique_ident::gen_ident(), - flags: &index_entry.2, - }; - let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; - let qr = QueryResult::FullResult { - uuid: mail_in_idx.uuid.clone(), - metadata: meta, - content: rfc822.to_vec(), - }; - - let mv = MailView::new(&qr, &mail_in_idx)?; - let (res_body, _seen) = mv.filter(&ap)?; - - let fattr = match res_body { - Body::Data(Data::Fetch { - seq: _seq, - items: attr, - }) => Ok(attr), - _ => Err(anyhow!("Not a fetch body")), - }?; - - assert_eq!(fattr.as_ref().len(), 1); - - let (sec, _orig, _data) = match &fattr.as_ref()[0] { - MessageDataItem::BodyExt { - section, - origin, - data, - } => Ok((section, origin, data)), - _ => Err(anyhow!("not a body ext message attribute")), - }?; - - assert_eq!(sec.as_ref().unwrap(), &Section::Header(None)); - - Ok(()) - } - - /// Future automated test. We use lossy utf8 conversion + lowercase everything, - /// so this test might allow invalid results. But at least it allows us to quickly test a - /// large variety of emails. - /// Keep in mind that special cases must still be tested manually! - #[test] - fn fetch_body() -> Result<()> { - let prefixes = [ - /* *** MY OWN DATASET *** */ - "tests/emails/dxflrs/0001_simple", - "tests/emails/dxflrs/0002_mime", - "tests/emails/dxflrs/0003_mime-in-mime", - "tests/emails/dxflrs/0004_msg-in-msg", - // eml_codec do not support continuation for the moment - //"tests/emails/dxflrs/0005_mail-parser-readme", - "tests/emails/dxflrs/0006_single-mime", - "tests/emails/dxflrs/0007_raw_msg_in_rfc822", - /* *** (STRANGE) RFC *** */ - //"tests/emails/rfc/000", // must return text/enriched, we return text/plain - //"tests/emails/rfc/001", // does not recognize the multipart/external-body, breaks the - // whole parsing - //"tests/emails/rfc/002", // wrong date in email - - //"tests/emails/rfc/003", // dovecot fixes \r\r: the bytes number is wrong + text/enriched - - /* *** THIRD PARTY *** */ - //"tests/emails/thirdparty/000", // dovecot fixes \r\r: the bytes number is wrong - //"tests/emails/thirdparty/001", // same - "tests/emails/thirdparty/002", // same - - /* *** LEGACY *** */ - //"tests/emails/legacy/000", // same issue with \r\r - ]; - - for pref in prefixes.iter() { - println!("{}", pref); - let txt = fs::read(format!("{}.eml", pref))?; - let oracle = fs::read(format!("{}.dovecot.body", pref))?; - let message = eml_codec::parse_message(&txt).unwrap().1; - - let test_repr = Response::Data(Data::Fetch { - seq: NonZeroU32::new(1).unwrap(), - items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure( - &message.child, - false, - )?)), - }); - let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); - let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); - - let oracle_str = - format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); - - println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); - //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); - assert_eq!(test_str, oracle_str); - } - - Ok(()) - } -} diff --git a/aero-proto/imap/mime_view.rs b/aero-proto/imap/mime_view.rs deleted file mode 100644 index 8bbbd2d..0000000 --- a/aero-proto/imap/mime_view.rs +++ /dev/null @@ -1,580 +0,0 @@ -use std::borrow::Cow; -use std::collections::HashSet; -use std::num::NonZeroU32; - -use anyhow::{anyhow, bail, Result}; - -use imap_codec::imap_types::body::{ - BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, - SpecificFields, -}; -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::{ - header, mime, mime::r#type::Deductible, part::composite, part::discrete, part::AnyPart, -}; - -use crate::imap::imf_view::ImfView; - -pub enum BodySection<'a> { - Full(Cow<'a, [u8]>), - Slice { - body: Cow<'a, [u8]>, - origin_octet: u32, - }, -} - -/// Logic for BODY[
]<> -/// Works in 3 times: -/// 1. Find the section (RootMime::subset) -/// 2. Apply the extraction logic (SelectedMime::extract), like TEXT, HEADERS, etc. -/// 3. Keep only the given subset provided by partial -/// -/// Example of message sections: -/// -/// ``` -/// HEADER ([RFC-2822] header of the message) -/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 1 TEXT/PLAIN -/// 2 APPLICATION/OCTET-STREAM -/// 3 MESSAGE/RFC822 -/// 3.HEADER ([RFC-2822] header of the message) -/// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 3.1 TEXT/PLAIN -/// 3.2 APPLICATION/OCTET-STREAM -/// 4 MULTIPART/MIXED -/// 4.1 IMAGE/GIF -/// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) -/// 4.2 MESSAGE/RFC822 -/// 4.2.HEADER ([RFC-2822] header of the message) -/// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED -/// 4.2.1 TEXT/PLAIN -/// 4.2.2 MULTIPART/ALTERNATIVE -/// 4.2.2.1 TEXT/PLAIN -/// 4.2.2.2 TEXT/RICHTEXT -/// ``` -pub fn body_ext<'a>( - part: &'a AnyPart<'a>, - section: &'a Option>, - partial: &'a Option<(u32, NonZeroU32)>, -) -> Result> { - let root_mime = NodeMime(part); - let (extractor, path) = SubsettedSection::from(section); - let selected_mime = root_mime.subset(path)?; - let extracted_full = selected_mime.extract(&extractor)?; - Ok(extracted_full.to_body_section(partial)) -} - -/// Logic for BODY and BODYSTRUCTURE -/// -/// ```raw -/// b fetch 29878:29879 (BODY) -/// * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) -/// * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) -/// ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ -/// | | | | | | number of lines -/// | | | | | size -/// | | | | content transfer encoding -/// | | | description -/// | | id -/// | parameter list -/// b OK Fetch completed (0.001 + 0.000 secs). -/// ``` -pub fn bodystructure(part: &AnyPart, is_ext: bool) -> Result> { - NodeMime(part).structure(is_ext) -} - -/// NodeMime -/// -/// Used for recursive logic on MIME. -/// See SelectedMime for inspection. -struct NodeMime<'a>(&'a AnyPart<'a>); -impl<'a> NodeMime<'a> { - /// A MIME object is a tree of elements. - /// The path indicates which element must be picked. - /// This function returns the picked element as the new view - fn subset(self, path: Option<&'a FetchPart>) -> Result> { - match path { - None => Ok(SelectedMime(self.0)), - Some(v) => self.rec_subset(v.0.as_ref()), - } - } - - fn rec_subset(self, path: &'a [NonZeroU32]) -> Result { - if path.is_empty() { - Ok(SelectedMime(self.0)) - } else { - match self.0 { - AnyPart::Mult(x) => { - let next = Self(x.children - .get(path[0].get() as usize - 1) - .ok_or(anyhow!("Unable to resolve subpath {:?}, current multipart has only {} elements", path, x.children.len()))?); - next.rec_subset(&path[1..]) - }, - AnyPart::Msg(x) => { - let next = Self(x.child.as_ref()); - next.rec_subset(path) - }, - _ => bail!("You tried to access a subpart on an atomic part (text or binary). Unresolved subpath {:?}", path), - } - } - } - - fn structure(&self, is_ext: bool) -> Result> { - match self.0 { - AnyPart::Txt(x) => NodeTxt(self, x).structure(is_ext), - AnyPart::Bin(x) => NodeBin(self, x).structure(is_ext), - AnyPart::Mult(x) => NodeMult(self, x).structure(is_ext), - AnyPart::Msg(x) => NodeMsg(self, x).structure(is_ext), - } - } -} - -//---------------------------------------------------------- - -/// A FetchSection must be handled in 2 times: -/// - First we must extract the MIME part -/// - Then we must process it as desired -/// The given struct mixes both work, so -/// we separate this work here. -enum SubsettedSection<'a> { - Part, - Header, - HeaderFields(&'a Vec1>), - HeaderFieldsNot(&'a Vec1>), - Text, - Mime, -} -impl<'a> SubsettedSection<'a> { - fn from(section: &'a Option) -> (Self, Option<&'a FetchPart>) { - match section { - Some(FetchSection::Text(maybe_part)) => (Self::Text, maybe_part.as_ref()), - Some(FetchSection::Header(maybe_part)) => (Self::Header, maybe_part.as_ref()), - Some(FetchSection::HeaderFields(maybe_part, fields)) => { - (Self::HeaderFields(fields), maybe_part.as_ref()) - } - Some(FetchSection::HeaderFieldsNot(maybe_part, fields)) => { - (Self::HeaderFieldsNot(fields), maybe_part.as_ref()) - } - Some(FetchSection::Mime(part)) => (Self::Mime, Some(part)), - Some(FetchSection::Part(part)) => (Self::Part, Some(part)), - None => (Self::Part, None), - } - } -} - -/// Used for current MIME inspection -/// -/// See NodeMime for recursive logic -pub struct SelectedMime<'a>(pub &'a AnyPart<'a>); -impl<'a> SelectedMime<'a> { - pub fn header_value(&'a self, to_match_ext: &[u8]) -> Option<&'a [u8]> { - let to_match = to_match_ext.to_ascii_lowercase(); - - self.eml_mime() - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .find(|(k, _)| k.to_ascii_lowercase() == to_match) - .map(|(_, v)| v) - .copied() - } - - /// The subsetted fetch section basically tells us the - /// extraction logic to apply on our selected MIME. - /// This function acts as a router for these logic. - fn extract(&self, extractor: &SubsettedSection<'a>) -> Result> { - match extractor { - SubsettedSection::Text => self.text(), - SubsettedSection::Header => self.header(), - SubsettedSection::HeaderFields(fields) => self.header_fields(fields, false), - SubsettedSection::HeaderFieldsNot(fields) => self.header_fields(fields, true), - SubsettedSection::Part => self.part(), - SubsettedSection::Mime => self.mime(), - } - } - - fn mime(&self) -> Result> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.mime.fields.raw, - AnyPart::Bin(p) => p.mime.fields.raw, - AnyPart::Msg(p) => p.child.mime().raw, - AnyPart::Mult(p) => p.mime.fields.raw, - }; - Ok(ExtractedFull(bytes.into())) - } - - fn part(&self) -> Result> { - let bytes = match &self.0 { - AnyPart::Txt(p) => p.body, - AnyPart::Bin(p) => p.body, - AnyPart::Msg(p) => p.raw_part, - AnyPart::Mult(_) => bail!("Multipart part has no body"), - }; - Ok(ExtractedFull(bytes.to_vec().into())) - } - - fn eml_mime(&self) -> &eml_codec::mime::NaiveMIME<'_> { - match &self.0 { - AnyPart::Msg(msg) => msg.child.mime(), - other => other.mime(), - } - } - - /// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part - /// specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of - /// field-name (as defined in [RFC-2822]) names, and return a - /// subset of the header. The subset returned by HEADER.FIELDS - /// contains only those header fields with a field-name that - /// matches one of the names in the list; similarly, the subset - /// returned by HEADER.FIELDS.NOT contains only the header fields - /// with a non-matching field-name. The field-matching is - /// case-insensitive but otherwise exact. - fn header_fields( - &self, - fields: &'a Vec1>, - invert: bool, - ) -> Result> { - // Build a lowercase ascii hashset with the fields to fetch - let index = fields - .as_ref() - .iter() - .map(|x| { - match x { - AString::Atom(a) => a.inner().as_bytes(), - AString::String(IString::Literal(l)) => l.as_ref(), - AString::String(IString::Quoted(q)) => q.inner().as_bytes(), - } - .to_ascii_lowercase() - }) - .collect::>(); - - // Extract MIME headers - let mime = self.eml_mime(); - - // Filter our MIME headers based on the field index - // 1. Keep only the correctly formatted headers - // 2. Keep only based on the index presence or absence - // 3. Reduce as a byte vector - let buffer = mime - .kv - .iter() - .filter_map(|field| match field { - header::Field::Good(header::Kv2(k, v)) => Some((k, v)), - _ => None, - }) - .filter(|(k, _)| index.contains(&k.to_ascii_lowercase()) ^ invert) - .fold(vec![], |mut acc, (k, v)| { - acc.extend(*k); - acc.extend(b": "); - acc.extend(*v); - acc.extend(b"\r\n"); - acc - }); - - Ok(ExtractedFull(buffer.into())) - } - - /// The HEADER [...] part specifiers refer to the [RFC-2822] header of the message or of - /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. - /// ```raw - /// HEADER ([RFC-2822] header of the message) - /// ``` - fn header(&self) -> Result> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_headers.into())) - } - - /// The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. - fn text(&self) -> Result> { - let msg = self - .0 - .as_message() - .ok_or(anyhow!("Selected part must be a message/rfc822"))?; - Ok(ExtractedFull(msg.raw_body.into())) - } - - // ------------ - - /// Basic field of a MIME part that is - /// common to all parts - fn basic_fields(&self) -> Result> { - let sz = match self.0 { - AnyPart::Txt(x) => x.body.len(), - AnyPart::Bin(x) => x.body.len(), - AnyPart::Msg(x) => x.raw_part.len(), - AnyPart::Mult(_) => 0, - }; - let m = self.0.mime(); - let parameter_list = m - .ctype - .as_ref() - .map(|x| { - x.params - .iter() - .map(|p| { - ( - IString::try_from(String::from_utf8_lossy(p.name).to_string()), - IString::try_from(p.value.to_string()), - ) - }) - .filter(|(k, v)| k.is_ok() && v.is_ok()) - .map(|(k, v)| (k.unwrap(), v.unwrap())) - .collect() - }) - .unwrap_or(vec![]); - - Ok(BasicFields { - parameter_list, - id: NString( - m.id.as_ref() - .and_then(|ci| IString::try_from(ci.to_string()).ok()), - ), - description: NString( - m.description - .as_ref() - .and_then(|cd| IString::try_from(cd.to_string()).ok()), - ), - content_transfer_encoding: match m.transfer_encoding { - mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"), - mime::mechanism::Mechanism::Binary => unchecked_istring("binary"), - mime::mechanism::Mechanism::QuotedPrintable => { - unchecked_istring("quoted-printable") - } - mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"), - _ => unchecked_istring("7bit"), - }, - // @FIXME we can't compute the size of the message currently... - size: u32::try_from(sz)?, - }) - } -} - -// --------------------------- -struct NodeMsg<'a>(&'a NodeMime<'a>, &'a composite::Message<'a>); -impl<'a> NodeMsg<'a> { - fn structure(&self, is_ext: bool) -> Result> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Message { - envelope: Box::new(ImfView(&self.1.imf).message_envelope()), - body_structure: Box::new(NodeMime(&self.1.child).structure(is_ext)?), - number_of_lines: nol(self.1.raw_part), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>); -impl<'a> NodeMult<'a> { - fn structure(&self, is_ext: bool) -> Result> { - let itype = &self.1.mime.interpreted_type; - let subtype = IString::try_from(itype.subtype.to_string()) - .unwrap_or(unchecked_istring("alternative")); - - let inner_bodies = self - .1 - .children - .iter() - .filter_map(|inner| NodeMime(&inner).structure(is_ext).ok()) - .collect::>(); - - Vec1::validate(&inner_bodies)?; - let bodies = Vec1::unvalidated(inner_bodies); - - Ok(BodyStructure::Multi { - bodies, - subtype, - extension_data: match is_ext { - true => Some(MultiPartExtensionData { - parameter_list: vec![( - IString::try_from("boundary").unwrap(), - IString::try_from(self.1.mime.interpreted_type.boundary.to_string())?, - )], - tail: None, - }), - _ => None, - }, - }) - } -} -struct NodeTxt<'a>(&'a NodeMime<'a>, &'a discrete::Text<'a>); -impl<'a> NodeTxt<'a> { - fn structure(&self, is_ext: bool) -> Result> { - let mut basic = SelectedMime(self.0 .0).basic_fields()?; - - // Get the interpreted content type, set it - let itype = match &self.1.mime.interpreted_type { - Deductible::Inferred(v) | Deductible::Explicit(v) => v, - }; - let subtype = - IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("plain")); - - // Add charset to the list of parameters if we know it has been inferred as it will be - // missing from the parsed content. - if let Deductible::Inferred(charset) = &itype.charset { - basic.parameter_list.push(( - unchecked_istring("charset"), - IString::try_from(charset.to_string()).unwrap_or(unchecked_istring("us-ascii")), - )); - } - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Text { - subtype, - number_of_lines: nol(self.1.body), - }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -struct NodeBin<'a>(&'a NodeMime<'a>, &'a discrete::Binary<'a>); -impl<'a> NodeBin<'a> { - fn structure(&self, is_ext: bool) -> Result> { - let basic = SelectedMime(self.0 .0).basic_fields()?; - - let default = mime::r#type::NaiveType { - main: &b"application"[..], - sub: &b"octet-stream"[..], - params: vec![], - }; - let ct = self.1.mime.fields.ctype.as_ref().unwrap_or(&default); - - let r#type = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type type given"), - ))?; - - let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err( - anyhow!("Unable to build IString from given Content-Type subtype given"), - ))?; - - Ok(BodyStructure::Single { - body: FetchBody { - basic, - specific: SpecificFields::Basic { r#type, subtype }, - }, - extension_data: match is_ext { - true => Some(SinglePartExtensionData { - md5: NString(None), - tail: None, - }), - _ => None, - }, - }) - } -} - -// --------------------------- - -struct ExtractedFull<'a>(Cow<'a, [u8]>); -impl<'a> ExtractedFull<'a> { - /// It is possible to fetch a substring of the designated text. - /// This is done by appending an open angle bracket ("<"), the - /// octet position of the first desired octet, a period, the - /// maximum number of octets desired, and a close angle bracket - /// (">") to the part specifier. If the starting octet is beyond - /// the end of the text, an empty string is returned. - /// - /// Any partial fetch that attempts to read beyond the end of the - /// text is truncated as appropriate. A partial fetch that starts - /// at octet 0 is returned as a partial fetch, even if this - /// truncation happened. - /// - /// Note: This means that BODY[]<0.2048> of a 1500-octet message - /// will return BODY[]<0> with a literal of size 1500, not - /// BODY[]. - /// - /// Note: A substring fetch of a HEADER.FIELDS or - /// HEADER.FIELDS.NOT part specifier is calculated after - /// subsetting the header. - fn to_body_section(self, partial: &'_ Option<(u32, NonZeroU32)>) -> BodySection<'a> { - match partial { - Some((begin, len)) => self.partialize(*begin, *len), - None => BodySection::Full(self.0), - } - } - - fn partialize(self, begin: u32, len: NonZeroU32) -> BodySection<'a> { - // Asked range is starting after the end of the content, - // returning an empty buffer - if begin as usize > self.0.len() { - return BodySection::Slice { - body: Cow::Borrowed(&[][..]), - origin_octet: begin, - }; - } - - // Asked range is ending after the end of the content, - // slice only the beginning of the buffer - if (begin + len.get()) as usize >= self.0.len() { - return BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => Cow::Borrowed(&body[begin as usize..]), - Cow::Owned(body) => Cow::Owned(body[begin as usize..].to_vec()), - }, - origin_octet: begin, - }; - } - - // Range is included inside the considered content, - // this is the "happy case" - BodySection::Slice { - body: match self.0 { - Cow::Borrowed(body) => { - Cow::Borrowed(&body[begin as usize..(begin + len.get()) as usize]) - } - Cow::Owned(body) => { - Cow::Owned(body[begin as usize..(begin + len.get()) as usize].to_vec()) - } - }, - origin_octet: begin, - } - } -} - -/// ---- LEGACY - -/// s is set to static to ensure that only compile time values -/// checked by developpers are passed. -fn unchecked_istring(s: &'static str) -> IString { - IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") -} - -// Number Of Lines -fn nol(input: &[u8]) -> u32 { - input - .iter() - .filter(|x| **x == b'\n') - .count() - .try_into() - .unwrap_or(0) -} diff --git a/aero-proto/imap/mod.rs b/aero-proto/imap/mod.rs deleted file mode 100644 index 02ab9ce..0000000 --- a/aero-proto/imap/mod.rs +++ /dev/null @@ -1,421 +0,0 @@ -mod attributes; -mod capability; -mod command; -mod flags; -mod flow; -mod imf_view; -mod index; -mod mail_view; -mod mailbox_view; -mod mime_view; -mod request; -mod response; -mod search; -mod session; - -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, Context, Result}; -use futures::stream::{FuturesUnordered, StreamExt}; - -use tokio::net::TcpListener; -use tokio::sync::mpsc; -use tokio::sync::watch; - -use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status}; -use imap_codec::imap_types::{core::Text, response::Greeting}; -use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; -use imap_flow::stream::AnyStream; -use rustls_pemfile::{certs, private_key}; -use tokio_rustls::TlsAcceptor; - -use crate::config::{ImapConfig, ImapUnsecureConfig}; -use crate::imap::capability::ServerCapability; -use crate::imap::request::Request; -use crate::imap::response::{Body, ResponseOrIdle}; -use crate::imap::session::Instance; -use crate::login::ArcLoginProvider; - -/// Server is a thin wrapper to register our Services in BàL -pub struct Server { - bind_addr: SocketAddr, - login_provider: ArcLoginProvider, - capabilities: ServerCapability, - tls: Option, -} - -#[derive(Clone)] -struct ClientContext { - addr: SocketAddr, - login_provider: ArcLoginProvider, - must_exit: watch::Receiver, - server_capabilities: ServerCapability, -} - -pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result { - let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open( - config.certs, - )?)) - .collect::, _>>()?; - 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() - .with_single_cert(loaded_certs, loaded_key)?; - let acceptor = TlsAcceptor::from(Arc::new(tls_config)); - - Ok(Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: Some(acceptor), - }) -} - -pub fn new_unsecure(config: ImapUnsecureConfig, login: ArcLoginProvider) -> Server { - Server { - bind_addr: config.bind_addr, - login_provider: login, - capabilities: ServerCapability::default(), - tls: None, - } -} - -impl Server { - pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!("IMAP server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - tracing::info!("IMAP: accepted connection from {}", remote_addr); - let stream = match self.tls.clone() { - Some(acceptor) => { - let stream = match acceptor.accept(socket).await { - Ok(v) => v, - Err(e) => { - tracing::error!(err=?e, "TLS negociation failed"); - continue; - } - }; - AnyStream::new(stream) - } - None => AnyStream::new(socket), - }; - - let client = ClientContext { - addr: remote_addr.clone(), - login_provider: self.login_provider.clone(), - must_exit: must_exit.clone(), - server_capabilities: self.capabilities.clone(), - }; - let conn = tokio::spawn(NetLoop::handler(client, stream)); - connections.push(conn); - } - drop(tcp); - - tracing::info!("IMAP server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -use std::sync::Arc; -use tokio::sync::mpsc::*; -use tokio::sync::Notify; -use tokio_util::bytes::BytesMut; - -const PIPELINABLE_COMMANDS: usize = 64; - -// @FIXME a full refactor of this part of the code will be needed sooner or later -struct NetLoop { - ctx: ClientContext, - server: ServerFlow, - cmd_tx: Sender, - resp_rx: UnboundedReceiver, -} - -impl NetLoop { - async fn handler(ctx: ClientContext, sock: AnyStream) { - let addr = ctx.addr.clone(); - - let mut nl = match Self::new(ctx, sock).await { - Ok(nl) => { - tracing::debug!(addr=?addr, "netloop successfully initialized"); - nl - } - Err(e) => { - tracing::error!(addr=?addr, err=?e, "netloop can not be initialized, closing session"); - return; - } - }; - - match nl.core().await { - Ok(()) => { - tracing::debug!("closing successful netloop core for {:?}", addr); - } - Err(e) => { - tracing::error!("closing errored netloop core for {:?}: {}", addr, e); - } - } - } - - async fn new(ctx: ClientContext, sock: AnyStream) -> Result { - 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, - opts, - Greeting::ok( - Some(Code::Capability(ctx.server_capabilities.to_vec())), - "Aerogramme", - ) - .unwrap(), - ) - .await?; - - // Start a mailbox session in background - let (cmd_tx, cmd_rx) = mpsc::channel::(PIPELINABLE_COMMANDS); - let (resp_tx, resp_rx) = mpsc::unbounded_channel::(); - tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx)); - - // Return the object - Ok(NetLoop { - ctx, - server, - cmd_tx, - resp_rx, - }) - } - - /// Coms with the background session - async fn session( - ctx: ClientContext, - mut cmd_rx: Receiver, - resp_tx: UnboundedSender, - ) -> () { - let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); - loop { - let cmd = match cmd_rx.recv().await { - None => break, - Some(cmd_recv) => cmd_recv, - }; - - tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); - let maybe_response = session.request(cmd).await; - tracing::debug!(cmd=?maybe_response, sock=%ctx.addr, "response"); - - match resp_tx.send(maybe_response) { - Err(_) => break, - Ok(_) => (), - }; - } - tracing::info!("runner is quitting"); - } - - async fn core(&mut self) -> Result<()> { - let mut maybe_idle: Option> = None; - loop { - 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()); - }, - }; - } - } - - /* - async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc) -> Result { - // Flush send - loop { - tracing::trace!("flush server send"); - match self.server.progress_send().await? { - Some(..) => continue, - None => break, - } - } - - tokio::select! { - // Receiving IDLE event from background - 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), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.server.enqueue_status(response.completion); - return Ok(LoopMode::Interactive) - }, - // 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), - Body::Status(s) => self.server.enqueue_status(s), - }; - } - self.cmd_tx.try_send(Request::Idle)?; - return Ok(LoopMode::Idle(buff, stop)) - }, - - // Session crashed - None => { - self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); - tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); - return Ok(LoopMode::Interactive) - }, - - // Session can't start idling while already idling, it's a logic error! - Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"), - }, - - // User is trying to interact with us - 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(_) => { - 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."), - }; - - return Ok(LoopMode::Idle(buff, stop)) - }, - - // 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) - }, - }; - }*/ -} diff --git a/aero-proto/imap/request.rs b/aero-proto/imap/request.rs deleted file mode 100644 index cff18a3..0000000 --- a/aero-proto/imap/request.rs +++ /dev/null @@ -1,9 +0,0 @@ -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; - -#[derive(Debug)] -pub enum Request { - ImapCommand(Command<'static>), - IdleStart(Tag<'static>), - IdlePoll, -} diff --git a/aero-proto/imap/response.rs b/aero-proto/imap/response.rs deleted file mode 100644 index b6a0e98..0000000 --- a/aero-proto/imap/response.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Result; -use imap_codec::imap_types::command::Command; -use imap_codec::imap_types::core::Tag; -use imap_codec::imap_types::response::{Code, Data, Status}; -use std::sync::Arc; -use tokio::sync::Notify; - -#[derive(Debug)] -pub enum Body<'a> { - Data(Data<'a>), - Status(Status<'a>), -} - -pub struct ResponseBuilder<'a> { - tag: Option>, - code: Option>, - text: String, - body: Vec>, -} - -impl<'a> ResponseBuilder<'a> { - pub fn to_req(mut self, cmd: &Command<'a>) -> Self { - self.tag = Some(cmd.tag.clone()); - self - } - pub fn tag(mut self, tag: Tag<'a>) -> Self { - self.tag = Some(tag); - self - } - - pub fn message(mut self, txt: impl Into) -> Self { - self.text = txt.into(); - self - } - - pub fn code(mut self, code: Code<'a>) -> Self { - self.code = Some(code); - self - } - - pub fn data(mut self, data: Data<'a>) -> Self { - self.body.push(Body::Data(data)); - self - } - - pub fn many_data(mut self, data: Vec>) -> Self { - for d in data.into_iter() { - self = self.data(d); - } - self - } - - #[allow(dead_code)] - pub fn info(mut self, status: Status<'a>) -> Self { - self.body.push(Body::Status(status)); - self - } - - #[allow(dead_code)] - pub fn many_info(mut self, status: Vec>) -> Self { - for d in status.into_iter() { - self = self.info(d); - } - self - } - - pub fn set_body(mut self, body: Vec>) -> Self { - self.body = body; - self - } - - pub fn ok(self) -> Result> { - Ok(Response { - completion: Status::ok(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn no(self) -> Result> { - Ok(Response { - completion: Status::no(self.tag, self.code, self.text)?, - body: self.body, - }) - } - - pub fn bad(self) -> Result> { - Ok(Response { - completion: Status::bad(self.tag, self.code, self.text)?, - body: self.body, - }) - } -} - -#[derive(Debug)] -pub struct Response<'a> { - pub body: Vec>, - pub completion: Status<'a>, -} - -impl<'a> Response<'a> { - pub fn build() -> ResponseBuilder<'a> { - ResponseBuilder { - tag: None, - code: None, - text: "".to_string(), - body: vec![], - } - } - - pub fn bye() -> Result> { - Ok(Response { - completion: Status::bye(None, "bye")?, - body: vec![], - }) - } -} - -#[derive(Debug)] -pub enum ResponseOrIdle { - Response(Response<'static>), - IdleAccept(Arc), - IdleReject(Response<'static>), - IdleEvent(Vec>), -} diff --git a/aero-proto/imap/search.rs b/aero-proto/imap/search.rs deleted file mode 100644 index 37a7e9e..0000000 --- a/aero-proto/imap/search.rs +++ /dev/null @@ -1,477 +0,0 @@ -use std::num::{NonZeroU32, NonZeroU64}; - -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::QueryScope; - -pub enum SeqType { - Undefined, - NonUid, - Uid, -} -impl SeqType { - pub fn is_uid(&self) -> bool { - matches!(self, Self::Uid) - } -} - -pub struct Criteria<'a>(pub &'a SearchKey<'a>); -impl<'a> Criteria<'a> { - /// Returns a set of email identifiers that is greater or equal - /// to the set of emails to return - pub fn to_sequence_set(&self) -> (SequenceSet, SeqType) { - match self.0 { - SearchKey::All => (sequence_set_all(), SeqType::Undefined), - SearchKey::SequenceSet(seq_set) => (seq_set.clone(), SeqType::NonUid), - SearchKey::Uid(seq_set) => (seq_set.clone(), SeqType::Uid), - SearchKey::Not(_inner) => { - tracing::debug!( - "using NOT in a search request is slow: it selects all identifiers" - ); - (sequence_set_all(), SeqType::Undefined) - } - SearchKey::Or(left, right) => { - tracing::debug!("using OR in a search request is slow: no deduplication is done"); - let (base, base_seqtype) = Self(&left).to_sequence_set(); - let (ext, ext_seqtype) = Self(&right).to_sequence_set(); - - // Check if we have a UID/ID conflict in fetching: now we don't know how to handle them - match (base_seqtype, ext_seqtype) { - (SeqType::Uid, SeqType::NonUid) | (SeqType::NonUid, SeqType::Uid) => { - (sequence_set_all(), SeqType::Undefined) - } - (SeqType::Undefined, x) | (x, _) => { - let mut new_vec = base.0.into_inner(); - new_vec.extend_from_slice(ext.0.as_ref()); - let seq = SequenceSet( - Vec1::try_from(new_vec) - .expect("merging non empty vec lead to non empty vec"), - ); - (seq, x) - } - } - } - SearchKey::And(search_list) => { - tracing::debug!( - "using AND in a search request is slow: no intersection is performed" - ); - // As we perform no intersection, we don't care if we mix uid or id. - // We only keep the smallest range, being it ID or UID, depending of - // which one has the less items. This is an approximation as UID ranges - // can have holes while ID ones can't. - search_list - .as_ref() - .iter() - .map(|crit| Self(&crit).to_sequence_set()) - .min_by(|(x, _), (y, _)| { - let x_size = approx_sequence_set_size(x); - let y_size = approx_sequence_set_size(y); - x_size.cmp(&y_size) - }) - .unwrap_or((sequence_set_all(), SeqType::Undefined)) - } - _ => (sequence_set_all(), SeqType::Undefined), - } - } - - /// Not really clever as we can have cases where we filter out - /// the email before needing to inspect its meta. - /// But for now we are seeking the most basic/stupid algorithm. - pub fn query_scope(&self) -> QueryScope { - use SearchKey::*; - match self.0 { - // Combinators - And(and_list) => and_list - .as_ref() - .iter() - .fold(QueryScope::Index, |prev, sk| { - prev.union(&Criteria(sk).query_scope()) - }), - Not(inner) => Criteria(inner).query_scope(), - Or(left, right) => Criteria(left) - .query_scope() - .union(&Criteria(right).query_scope()), - All => QueryScope::Index, - - // IMF Headers - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) => QueryScope::Partial, - // Internal Date is also stored in MailMeta - Before(_) | On(_) | Since(_) => QueryScope::Partial, - // Message size is also stored in MailMeta - Larger(_) | Smaller(_) => QueryScope::Partial, - // Text and Body require that we fetch the full content! - Text(_) | Body(_) => QueryScope::Full, - - _ => QueryScope::Index, - } - } - - pub fn is_modseq(&self) -> bool { - use SearchKey::*; - match self.0 { - And(and_list) => and_list - .as_ref() - .iter() - .any(|child| Criteria(child).is_modseq()), - Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(), - Not(child) => Criteria(child).is_modseq(), - ModSeq { .. } => true, - _ => false, - } - } - - /// Returns emails that we now for sure we want to keep - /// but also a second list of emails we need to investigate further by - /// fetching some remote data - pub fn filter_on_idx<'b>( - &self, - midx_list: &[&'b MailIndex<'b>], - ) -> (Vec<&'b MailIndex<'b>>, Vec<&'b MailIndex<'b>>) { - let (p1, p2): (Vec<_>, Vec<_>) = midx_list - .iter() - .map(|x| (x, self.is_keep_on_idx(x))) - .filter(|(_midx, decision)| decision.is_keep()) - .map(|(midx, decision)| (*midx, decision)) - .partition(|(_midx, decision)| matches!(decision, PartialDecision::Keep)); - - let to_keep = p1.into_iter().map(|(v, _)| v).collect(); - let to_fetch = p2.into_iter().map(|(v, _)| v).collect(); - (to_keep, to_fetch) - } - - // ---- - - /// Here we are doing a partial filtering: we do not have access - /// to the headers or to the body, so every time we encounter a rule - /// based on them, we need to keep it. - /// - /// @TODO Could be optimized on a per-email basis by also returning the QueryScope - /// when more information is needed! - fn is_keep_on_idx(&self, midx: &MailIndex) -> PartialDecision { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .fold(PartialDecision::Keep, |acc, cur| { - acc.and(&Criteria(cur).is_keep_on_idx(midx)) - }), - Or(left, right) => { - let left_decision = Criteria(left).is_keep_on_idx(midx); - let right_decision = Criteria(right).is_keep_on_idx(midx); - left_decision.or(&right_decision) - } - Not(expr) => Criteria(expr).is_keep_on_idx(midx).not(), - All => PartialDecision::Keep, - - // Sequence logic - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, midx).into(), - - // All the stuff we can't evaluate yet - Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) - | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) - | Text(_) | Body(_) => PartialDecision::Postpone, - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - PartialDecision::Discard - } - } - } - - /// @TODO we re-eveluate twice the same logic. The correct way would be, on each pass, - /// to simplify the searck query, by removing the elements that were already checked. - /// For example if we have AND(OR(seqid(X), body(Y)), body(X)), we can't keep for sure - /// 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. - pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool { - use SearchKey::*; - match self.0 { - // Combinator logic - And(expr_list) => expr_list - .as_ref() - .iter() - .all(|cur| Criteria(cur).is_keep_on_query(mail_view)), - Or(left, right) => { - Criteria(left).is_keep_on_query(mail_view) - || Criteria(right).is_keep_on_query(mail_view) - } - Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), - All => true, - - //@FIXME Reevaluating our previous logic... - maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), - maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), - ModSeq { - metadata_item, - modseq, - } => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(), - - // Filter on mail meta - Before(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive < search_naive.as_ref(), - _ => false, - }, - On(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive == search_naive.as_ref(), - _ => false, - }, - Since(search_naive) => match mail_view.stored_naive_date() { - Ok(msg_naive) => &msg_naive > search_naive.as_ref(), - _ => false, - }, - - // Message size is also stored in MailMeta - Larger(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - > *size_ref as usize - } - Smaller(size_ref) => { - mail_view - .query_result - .metadata() - .expect("metadata were fetched") - .rfc822_size - < *size_ref as usize - } - - // Filter on well-known headers - Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()), - Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()), - From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()), - Subject(txt) => mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()), - To(txt) => mail_view.is_header_contains_pattern(&b"to"[..], txt.as_ref()), - Header(hdr, txt) => mail_view.is_header_contains_pattern(hdr.as_ref(), txt.as_ref()), - - // Filter on Date header - SentBefore(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive < search_naive.as_ref()) - .unwrap_or(false), - SentOn(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive == search_naive.as_ref()) - .unwrap_or(false), - SentSince(search_naive) => mail_view - .imf() - .map(|imf| imf.naive_date().ok()) - .flatten() - .map(|msg_naive| &msg_naive > search_naive.as_ref()) - .unwrap_or(false), - - // Filter on the full content of the email - Text(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_part - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - Body(txt) => mail_view - .content - .as_msg() - .map(|msg| { - msg.raw_body - .windows(txt.as_ref().len()) - .any(|win| win == txt.as_ref()) - }) - .unwrap_or(false), - - unknown => { - tracing::error!("Unknown filter {:?}", unknown); - false - } - } - } -} - -// ---- Sequence things ---- -fn sequence_set_all() -> SequenceSet { - SequenceSet::from(Sequence::Range( - SeqOrUid::Value(NonZeroU32::MIN), - SeqOrUid::Asterisk, - )) -} - -// This is wrong as sequences can overlap -fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { - seq_set.0.as_ref().iter().fold(0u64, |acc, seq| { - acc.saturating_add(approx_sequence_size(seq)) - }) -} - -// This is wrong as sequence UID can have holes, -// as we don't know the number of messages in the mailbox also -// we gave to guess -fn approx_sequence_size(seq: &Sequence) -> u64 { - match seq { - Sequence::Single(_) => 1, - Sequence::Range(SeqOrUid::Asterisk, _) | Sequence::Range(_, SeqOrUid::Asterisk) => u64::MAX, - Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { - let x2 = x2.get() as i64; - let x1 = x1.get() as i64; - (x2 - x1).abs().try_into().unwrap_or(1) - } - } -} - -// --- Partial decision things ---- - -enum PartialDecision { - Keep, - Discard, - Postpone, -} -impl From for PartialDecision { - fn from(x: bool) -> Self { - match x { - true => PartialDecision::Keep, - _ => PartialDecision::Discard, - } - } -} -impl PartialDecision { - fn not(&self) -> Self { - match self { - Self::Keep => Self::Discard, - Self::Discard => Self::Keep, - Self::Postpone => Self::Postpone, - } - } - - fn or(&self, other: &Self) -> Self { - match (self, other) { - (Self::Keep, _) | (_, Self::Keep) => Self::Keep, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Discard, Self::Discard) => Self::Discard, - } - } - - fn and(&self, other: &Self) -> Self { - match (self, other) { - (Self::Discard, _) | (_, Self::Discard) => Self::Discard, - (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, - (Self::Keep, Self::Keep) => Self::Keep, - } - } - - fn is_keep(&self) -> bool { - !matches!(self, Self::Discard) - } -} - -// ----- Search Key things --- -fn is_sk_flag(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - Answered | Deleted | Draft | Flagged | Keyword(..) | New | Old | Recent | Seen - | Unanswered | Undeleted | Undraft | Unflagged | Unkeyword(..) | Unseen => true, - _ => false, - } -} - -fn is_keep_flag(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - Answered => midx.is_flag_set("\\Answered"), - Deleted => midx.is_flag_set("\\Deleted"), - Draft => midx.is_flag_set("\\Draft"), - Flagged => midx.is_flag_set("\\Flagged"), - Keyword(kw) => midx.is_flag_set(kw.inner()), - New => { - let is_recent = midx.is_flag_set("\\Recent"); - let is_seen = midx.is_flag_set("\\Seen"); - is_recent && !is_seen - } - Old => { - let is_recent = midx.is_flag_set("\\Recent"); - !is_recent - } - Recent => midx.is_flag_set("\\Recent"), - Seen => midx.is_flag_set("\\Seen"), - Unanswered => { - let is_answered = midx.is_flag_set("\\Recent"); - !is_answered - } - Undeleted => { - let is_deleted = midx.is_flag_set("\\Deleted"); - !is_deleted - } - Undraft => { - let is_draft = midx.is_flag_set("\\Draft"); - !is_draft - } - Unflagged => { - let is_flagged = midx.is_flag_set("\\Flagged"); - !is_flagged - } - Unkeyword(kw) => { - let is_keyword_set = midx.is_flag_set(kw.inner()); - !is_keyword_set - } - Unseen => { - let is_seen = midx.is_flag_set("\\Seen"); - !is_seen - } - - // Not flag logic - _ => unreachable!(), - } -} - -fn is_sk_seq(sk: &SearchKey) -> bool { - use SearchKey::*; - match sk { - SequenceSet(..) | Uid(..) => true, - _ => false, - } -} -fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { - use SearchKey::*; - match sk { - SequenceSet(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_i(seq)), - Uid(seq_set) => seq_set - .0 - .as_ref() - .iter() - .any(|seq| midx.is_in_sequence_uid(seq)), - _ => unreachable!(), - } -} - -fn is_keep_modseq( - filter: &Option, - modseq: &NonZeroU64, - midx: &MailIndex, -) -> bool { - if filter.is_some() { - tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet"); - } - modseq <= &midx.modseq -} diff --git a/aero-proto/imap/session.rs b/aero-proto/imap/session.rs deleted file mode 100644 index fa3232a..0000000 --- a/aero-proto/imap/session.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::imap::capability::{ClientCapability, ServerCapability}; -use crate::imap::command::{anonymous, authenticated, selected}; -use crate::imap::flow; -use crate::imap::request::Request; -use crate::imap::response::{Response, ResponseOrIdle}; -use crate::login::ArcLoginProvider; -use anyhow::{anyhow, bail, Context, Result}; -use imap_codec::imap_types::{command::Command, core::Tag}; - -//----- -pub struct Instance { - pub login_provider: ArcLoginProvider, - pub server_capabilities: ServerCapability, - pub client_capabilities: ClientCapability, - pub state: flow::State, -} -impl Instance { - pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { - let client_cap = ClientCapability::new(&cap); - Self { - login_provider, - state: flow::State::NotAuthenticated, - server_capabilities: cap, - client_capabilities: client_cap, - } - } - - pub async fn request(&mut self, req: Request) -> ResponseOrIdle { - match req { - Request::IdleStart(tag) => self.idle_init(tag), - Request::IdlePoll => self.idle_poll().await, - Request::ImapCommand(cmd) => self.command(cmd).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"); - ResponseOrIdle::Response(Response::bye().unwrap()) - } - } - } - - pub async fn idle_poll_happy(&mut self) -> Result { - 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"), - }; - - tokio::select! { - _ = stop.notified() => { - self.state.apply(flow::Transition::UnIdle)?; - return Ok(ResponseOrIdle::Response(Response::build() - .tag(tag.clone()) - .message("IDLE completed") - .ok()?)) - }, - change = mbx.idle_sync() => { - tracing::debug!("idle event"); - return Ok(ResponseOrIdle::IdleEvent(change?)); - } - } - } - - pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle { - // Command behavior is modulated by the state. - // To prevent state error, we handle the same command in separate code paths. - let (resp, tr) = match &mut self.state { - flow::State::NotAuthenticated => { - let ctx = anonymous::AnonymousContext { - req: &cmd, - login_provider: &self.login_provider, - server_capabilities: &self.server_capabilities, - }; - anonymous::dispatch(ctx).await - } - flow::State::Authenticated(ref user) => { - let ctx = authenticated::AuthenticatedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - }; - authenticated::dispatch(ctx).await - } - flow::State::Selected(ref user, ref mut mailbox, ref perm) => { - let ctx = selected::SelectedContext { - req: &cmd, - server_capabilities: &self.server_capabilities, - client_capabilities: &mut self.client_capabilities, - user, - mailbox, - perm, - }; - selected::dispatch(ctx).await - } - flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")), - flow::State::Logout => Response::build() - .tag(cmd.tag.clone()) - .message("No commands are allowed in the LOGOUT state.") - .bad() - .map(|r| (r, flow::Transition::None)), - } - .unwrap_or_else(|err| { - tracing::error!("Command error {:?} occured while processing {:?}", err, cmd); - ( - Response::build() - .to_req(&cmd) - .message("Internal error while processing command") - .bad() - .unwrap(), - flow::Transition::None, - ) - }); - - if let Err(e) = self.state.apply(tr) { - tracing::error!( - "Transition error {:?} occured while processing on command {:?}", - e, - cmd - ); - return ResponseOrIdle::Response(Response::build() - .to_req(&cmd) - .message( - "Internal error, processing command triggered an illegal IMAP state transition", - ) - .bad() - .unwrap()); - } - ResponseOrIdle::Response(resp) - - /*match &self.state { - flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()), - _ => ResponseOrIdle::Response(resp), - }*/ - } -} diff --git a/aero-proto/lmtp.rs b/aero-proto/lmtp.rs deleted file mode 100644 index dcd4bcc..0000000 --- a/aero-proto/lmtp.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::net::SocketAddr; -use std::{pin::Pin, sync::Arc}; - -use anyhow::Result; -use async_trait::async_trait; -use duplexify::Duplex; -use futures::{io, AsyncRead, AsyncReadExt, AsyncWrite}; -use futures::{ - stream, - stream::{FuturesOrdered, FuturesUnordered}, - StreamExt, -}; -use log::*; -use tokio::net::TcpListener; -use tokio::select; -use tokio::sync::watch; -use tokio_util::compat::*; - -use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode}; -use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata}; - -use crate::config::*; -use crate::login::*; -use crate::mail::incoming::EncryptedMessage; - -pub struct LmtpServer { - bind_addr: SocketAddr, - hostname: String, - login_provider: Arc, -} - -impl LmtpServer { - pub fn new( - config: LmtpConfig, - login_provider: Arc, - ) -> Arc { - Arc::new(Self { - bind_addr: config.bind_addr, - hostname: config.hostname, - login_provider, - }) - } - - pub async fn run(self: &Arc, mut must_exit: watch::Receiver) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - info!("LMTP server listening on {:#}", self.bind_addr); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - let (socket, remote_addr) = select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - info!("LMTP: accepted connection from {}", remote_addr); - - let conn = tokio::spawn(smtp_server::interact( - socket.compat(), - smtp_server::IsAlreadyTls::No, - (), - self.clone(), - )); - - connections.push(conn); - } - drop(tcp); - - info!("LMTP server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -// ---- - -pub struct Message { - to: Vec, -} - -#[async_trait] -impl Config for LmtpServer { - type Protocol = smtp_server::protocol::Lmtp; - - type ConnectionUserMeta = (); - type MailUserMeta = Message; - - fn hostname(&self, _conn_meta: &ConnectionMetadata<()>) -> &str { - &self.hostname - } - - async fn new_mail(&self, _conn_meta: &mut ConnectionMetadata<()>) -> Message { - Message { to: vec![] } - } - - async fn tls_accept( - &self, - _io: IO, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> io::Result>, Pin>>> - where - IO: Send + AsyncRead + AsyncWrite, - { - Err(io::Error::new( - io::ErrorKind::InvalidInput, - "TLS not implemented for LMTP server", - )) - } - - async fn filter_from( - &self, - from: Option, - _meta: &mut MailMetadata, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> Decision> { - Decision::Accept { - reply: reply::okay_from().convert(), - res: from, - } - } - - async fn filter_to( - &self, - to: Email, - meta: &mut MailMetadata, - _conn_meta: &mut ConnectionMetadata<()>, - ) -> Decision { - let to_str = match to.hostname.as_ref() { - Some(h) => format!("{}@{}", to.localpart, h), - None => to.localpart.to_string(), - }; - match self.login_provider.public_login(&to_str).await { - Ok(creds) => { - meta.user.to.push(creds); - Decision::Accept { - reply: reply::okay_to().convert(), - res: to, - } - } - Err(e) => Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], - }, - }, - } - } - - async fn handle_mail<'resp, R>( - &'resp self, - reader: &mut EscapedDataReader<'_, R>, - meta: MailMetadata, - _conn_meta: &'resp mut ConnectionMetadata<()>, - ) -> Pin> + Send + 'resp>> - where - R: Send + Unpin + AsyncRead, - { - let err_response_stream = |meta: MailMetadata, msg: String| { - Box::pin( - stream::iter(meta.user.to.into_iter()).map(move |_| Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(msg.clone())], - }, - }), - ) - }; - - let mut text = Vec::new(); - if let Err(e) = reader.read_to_end(&mut text).await { - 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), - Err(e) => return err_response_stream(meta, e.to_string()), - }; - - Box::pin( - meta.user - .to - .into_iter() - .map(move |creds| { - let encrypted_message = encrypted_message.clone(); - async move { - match encrypted_message.deliver_to(creds).await { - Ok(()) => Decision::Accept { - reply: reply::okay_mail().convert(), - res: (), - }, - Err(e) => Decision::Reject { - reply: Reply { - code: ReplyCode::POLICY_REASON, - ecode: None, - text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], - }, - }, - } - } - }) - .collect::>(), - ) - } -} diff --git a/aero-proto/sasl.rs b/aero-proto/sasl.rs deleted file mode 100644 index fe292e1..0000000 --- a/aero-proto/sasl.rs +++ /dev/null @@ -1,140 +0,0 @@ -use std::net::SocketAddr; - -use anyhow::{anyhow, bail, Result}; -use futures::stream::{FuturesUnordered, StreamExt}; -use tokio::io::BufStream; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; -use tokio::net::{TcpListener, TcpStream}; -use tokio::sync::watch; - -use aero_user::config::AuthConfig; -use aero_user::login::ArcLoginProvider; - - -pub struct AuthServer { - login_provider: ArcLoginProvider, - bind_addr: SocketAddr, -} - -impl AuthServer { - 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) -> Result<()> { - let tcp = TcpListener::bind(self.bind_addr).await?; - tracing::info!( - "SASL Authentication Protocol listening on {:#}", - self.bind_addr - ); - - let mut connections = FuturesUnordered::new(); - - while !*must_exit.borrow() { - let wait_conn_finished = async { - if connections.is_empty() { - futures::future::pending().await - } else { - connections.next().await - } - }; - - let (socket, remote_addr) = tokio::select! { - a = tcp.accept() => a?, - _ = wait_conn_finished => continue, - _ = must_exit.changed() => continue, - }; - - tracing::info!("AUTH: accepted connection from {}", remote_addr); - let conn = tokio::spawn( - NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(), - ); - - connections.push(conn); - } - drop(tcp); - - tracing::info!("AUTH server shutting down, draining remaining connections..."); - while connections.next().await.is_some() {} - - Ok(()) - } -} - -struct NetLoop { - login: ArcLoginProvider, - stream: BufStream, - stop: watch::Receiver, - state: State, - read_buf: Vec, - write_buf: BytesMut, -} - -impl NetLoop { - fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver) -> Self { - Self { - login, - stream: BufStream::new(stream), - state: State::Init, - stop, - read_buf: Vec::new(), - write_buf: BytesMut::new(), - } - } - - async fn run_error(self) { - match self.run().await { - Ok(()) => tracing::info!("Auth session succeeded"), - Err(e) => tracing::error!(err=?e, "Auth session failed"), - } - } - - async fn run(mut self) -> Result<()> { - loop { - tokio::select! { - read_res = self.stream.read_until(b'\n', &mut self.read_buf) => { - // Detect EOF / socket close - let bread = read_res?; - if bread == 0 { - tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session."); - return Ok(()) - } - - // Parse command - let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?; - tracing::trace!(cmd=?cmd, "Received command"); - - // Make some progress in our local state - self.state.progress(cmd, &self.login).await; - if matches!(self.state, State::Error) { - bail!("Internal state is in error, previous logs explain what went wrong"); - } - - // Build response - let srv_cmds = self.state.response(); - srv_cmds.iter().try_for_each(|r| { - tracing::trace!(cmd=?r, "Sent command"); - r.encode(&mut self.write_buf) - })?; - - // Send responses if at least one command response has been generated - if !srv_cmds.is_empty() { - self.stream.write_all(&self.write_buf).await?; - self.stream.flush().await?; - } - - // Reset buffers - self.read_buf.clear(); - self.write_buf.clear(); - }, - _ = self.stop.changed() => { - tracing::debug!("Server is stopping, quitting this runner"); - return Ok(()) - } - } - } - } -} diff --git a/aero-proto/src/dav.rs b/aero-proto/src/dav.rs new file mode 100644 index 0000000..2852d34 --- /dev/null +++ b/aero-proto/src/dav.rs @@ -0,0 +1,146 @@ +use std::net::SocketAddr; + +use anyhow::{anyhow, Result}; +use base64::Engine; +use hyper::service::service_fn; +use hyper::{Request, Response, body::Bytes}; +use hyper::server::conn::http1 as http; +use hyper_util::rt::TokioIo; +use http_body_util::Full; +use futures::stream::{FuturesUnordered, StreamExt}; +use tokio::net::TcpListener; +use tokio::sync::watch; + +use aero_user::config::DavUnsecureConfig; +use aero_user::login::ArcLoginProvider; +use aero_collections::user::User; + +pub struct Server { + bind_addr: SocketAddr, + login_provider: ArcLoginProvider, +} + +pub fn new_unsecure(config: DavUnsecureConfig, login: ArcLoginProvider) -> Server { + Server { + bind_addr: config.bind_addr, + login_provider: login, + } +} + +impl Server { + pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + tracing::info!("DAV server listening on {:#}", self.bind_addr); + + let mut connections = FuturesUnordered::new(); + while !*must_exit.borrow() { + let wait_conn_finished = async { + if connections.is_empty() { + futures::future::pending().await + } else { + connections.next().await + } + }; + let (socket, remote_addr) = tokio::select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + tracing::info!("Accepted connection from {}", remote_addr); + let stream = TokioIo::new(socket); + let login = self.login_provider.clone(); + let conn = tokio::spawn(async move { + //@FIXME should create a generic "public web" server on which "routers" could be + //abitrarily bound + //@FIXME replace with a handler supporting http2 and TLS + match http::Builder::new().serve_connection(stream, service_fn(|req: Request| { + let login = login.clone(); + async move { + auth(login, req).await + } + })).await { + Err(e) => tracing::warn!(err=?e, "connection failed"), + Ok(()) => tracing::trace!("connection terminated with success"), + } + }); + connections.push(conn); + } + drop(tcp); + + tracing::info!("Server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) + } +} + +//@FIXME We should not support only BasicAuth +async fn auth( + login: ArcLoginProvider, + req: Request, +) -> Result>> { + + let auth_val = match req.headers().get("Authorization") { + Some(hv) => hv.to_str()?, + None => return Ok(Response::builder() + .status(401) + .body(Full::new(Bytes::from("Missing Authorization field")))?), + }; + + let b64_creds_maybe_padded = match auth_val.split_once(" ") { + Some(("Basic", b64)) => b64, + _ => return Ok(Response::builder() + .status(400) + .body(Full::new(Bytes::from("Unsupported Authorization field")))?), + }; + + // base64urlencoded may have trailing equals, base64urlsafe has not + // theoretically authorization is padded but "be liberal in what you accept" + let b64_creds_clean = b64_creds_maybe_padded.trim_end_matches('='); + + // Decode base64 + let creds = base64::engine::general_purpose::STANDARD_NO_PAD.decode(b64_creds_clean)?; + let str_creds = std::str::from_utf8(&creds)?; + + // Split username and password + let (username, password) = str_creds + .split_once(':') + .ok_or(anyhow!("Missing colon in Authorization, can't split decoded value into a username/password pair"))?; + + // Call login provider + let creds = match login.login(username, password).await { + Ok(c) => c, + Err(_) => return Ok(Response::builder() + .status(401) + .body(Full::new(Bytes::from("Wrong credentials")))?), + }; + + // Build a user + let user = User::new(username.into(), creds).await?; + + // Call router with user + router(user, req).await +} + +async fn router(user: std::sync::Arc, req: Request) -> Result>> { + let path_segments: Vec<_> = req.uri().path().split("/").filter(|s| *s != "").collect(); + match path_segments.as_slice() { + [] => tracing::info!("root"), + [ username, ..] if *username != user.username => return Ok(Response::builder() + .status(403) + .body(Full::new(Bytes::from("Accessing other user ressources is not allowed")))?), + [ _ ] => tracing::info!("user home"), + [ _, "calendar" ] => tracing::info!("user calendars"), + [ _, "calendar", colname ] => tracing::info!(name=colname, "selected calendar"), + [ _, "calendar", colname, member ] => tracing::info!(name=colname, obj=member, "selected event"), + _ => return Ok(Response::builder() + .status(404) + .body(Full::new(Bytes::from("Resource not found")))?), + } + Ok(Response::new(Full::new(Bytes::from("Hello World!")))) +} + +#[allow(dead_code)] +async fn collections(_user: std::sync::Arc, _req: Request) -> Result>> { + unimplemented!(); +} diff --git a/aero-proto/src/imap/attributes.rs b/aero-proto/src/imap/attributes.rs new file mode 100644 index 0000000..89446a8 --- /dev/null +++ b/aero-proto/src/imap/attributes.rs @@ -0,0 +1,77 @@ +use imap_codec::imap_types::command::FetchModifier; +use imap_codec::imap_types::fetch::{MacroOrMessageDataItemNames, MessageDataItemName, Section}; + +/// Internal decisions based on fetched attributes +/// passed by the client + +pub struct AttributesProxy { + pub attrs: Vec>, +} +impl AttributesProxy { + pub fn new( + attrs: &MacroOrMessageDataItemNames<'static>, + modifiers: &[FetchModifier], + is_uid_fetch: bool, + ) -> Self { + // Expand macros + let mut fetch_attrs = match attrs { + MacroOrMessageDataItemNames::Macro(m) => { + use imap_codec::imap_types::fetch::Macro; + use MessageDataItemName::*; + match m { + Macro::All => vec![Flags, InternalDate, Rfc822Size, Envelope], + Macro::Fast => vec![Flags, InternalDate, Rfc822Size], + Macro::Full => vec![Flags, InternalDate, Rfc822Size, Envelope, Body], + _ => { + tracing::error!("unimplemented macro"); + vec![] + } + } + } + MacroOrMessageDataItemNames::MessageDataItemNames(a) => a.clone(), + }; + + // Handle uids + if is_uid_fetch && !fetch_attrs.contains(&MessageDataItemName::Uid) { + fetch_attrs.push(MessageDataItemName::Uid); + } + + // Handle inferred MODSEQ tag + let is_changed_since = modifiers + .iter() + .any(|m| matches!(m, FetchModifier::ChangedSince(..))); + if is_changed_since && !fetch_attrs.contains(&MessageDataItemName::ModSeq) { + fetch_attrs.push(MessageDataItemName::ModSeq); + } + + Self { attrs: fetch_attrs } + } + + pub fn is_enabling_condstore(&self) -> bool { + self.attrs + .iter() + .any(|x| matches!(x, MessageDataItemName::ModSeq)) + } + + pub fn need_body(&self) -> bool { + self.attrs.iter().any(|x| match x { + MessageDataItemName::Body + | MessageDataItemName::Rfc822 + | MessageDataItemName::Rfc822Text + | MessageDataItemName::BodyStructure => true, + + MessageDataItemName::BodyExt { + section: Some(section), + partial: _, + peek: _, + } => match section { + Section::Header(None) + | Section::HeaderFields(None, _) + | Section::HeaderFieldsNot(None, _) => false, + _ => true, + }, + MessageDataItemName::BodyExt { .. } => true, + _ => false, + }) + } +} diff --git a/aero-proto/src/imap/capability.rs b/aero-proto/src/imap/capability.rs new file mode 100644 index 0000000..c76b51c --- /dev/null +++ b/aero-proto/src/imap/capability.rs @@ -0,0 +1,159 @@ +use imap_codec::imap_types::command::{FetchModifier, SelectExamineModifier, StoreModifier}; +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; + +use crate::imap::attributes::AttributesProxy; + +fn capability_unselect() -> Capability<'static> { + Capability::try_from("UNSELECT").unwrap() +} + +fn capability_condstore() -> Capability<'static> { + Capability::try_from("CONDSTORE").unwrap() +} + +fn capability_uidplus() -> Capability<'static> { + Capability::try_from("UIDPLUS").unwrap() +} + +fn capability_liststatus() -> Capability<'static> { + Capability::try_from("LIST-STATUS").unwrap() +} + +/* +fn capability_qresync() -> Capability<'static> { + Capability::try_from("QRESYNC").unwrap() +} +*/ + +#[derive(Debug, Clone)] +pub struct ServerCapability(HashSet>); + +impl Default for ServerCapability { + fn default() -> Self { + Self(HashSet::from([ + Capability::Imap4Rev1, + Capability::Enable, + Capability::Move, + Capability::LiteralPlus, + Capability::Idle, + capability_unselect(), + capability_condstore(), + capability_uidplus(), + capability_liststatus(), + //capability_qresync(), + ])) + } +} + +impl ServerCapability { + pub fn to_vec(&self) -> Vec1> { + self.0 + .iter() + .map(|v| v.clone()) + .collect::>() + .try_into() + .unwrap() + } + + #[allow(dead_code)] + pub fn support(&self, cap: &Capability<'static>) -> bool { + self.0.contains(cap) + } +} + +#[derive(Clone)] +pub enum ClientStatus { + NotSupportedByServer, + Disabled, + Enabled, +} +impl ClientStatus { + pub fn is_enabled(&self) -> bool { + matches!(self, Self::Enabled) + } + + pub fn enable(&self) -> Self { + match self { + Self::Disabled => Self::Enabled, + other => other.clone(), + } + } +} + +pub struct ClientCapability { + pub condstore: ClientStatus, + pub utf8kind: Option, +} + +impl ClientCapability { + pub fn new(sc: &ServerCapability) -> Self { + Self { + condstore: match sc.0.contains(&capability_condstore()) { + true => ClientStatus::Disabled, + _ => ClientStatus::NotSupportedByServer, + }, + utf8kind: None, + } + } + + pub fn enable_condstore(&mut self) { + self.condstore = self.condstore.enable(); + } + + pub fn attributes_enable(&mut self, ap: &AttributesProxy) { + if ap.is_enabling_condstore() { + self.enable_condstore() + } + } + + pub fn fetch_modifiers_enable(&mut self, mods: &[FetchModifier]) { + if mods + .iter() + .any(|x| matches!(x, FetchModifier::ChangedSince(..))) + { + self.enable_condstore() + } + } + + pub fn store_modifiers_enable(&mut self, mods: &[StoreModifier]) { + if mods + .iter() + .any(|x| matches!(x, StoreModifier::UnchangedSince(..))) + { + self.enable_condstore() + } + } + + pub fn select_enable(&mut self, mods: &[SelectExamineModifier]) { + for m in mods.iter() { + match m { + SelectExamineModifier::Condstore => self.enable_condstore(), + } + } + } + + pub fn try_enable( + &mut self, + caps: &[CapabilityEnable<'static>], + ) -> Vec> { + let mut enabled = vec![]; + for cap in caps { + match cap { + CapabilityEnable::CondStore if matches!(self.condstore, ClientStatus::Disabled) => { + self.condstore = ClientStatus::Enabled; + enabled.push(cap.clone()); + } + CapabilityEnable::Utf8(kind) if Some(kind) != self.utf8kind.as_ref() => { + self.utf8kind = Some(kind.clone()); + enabled.push(cap.clone()); + } + _ => (), + } + } + + enabled + } +} diff --git a/aero-proto/src/imap/command/anonymous.rs b/aero-proto/src/imap/command/anonymous.rs new file mode 100644 index 0000000..2848c30 --- /dev/null +++ b/aero-proto/src/imap/command/anonymous.rs @@ -0,0 +1,84 @@ +use anyhow::Result; +use imap_codec::imap_types::command::{Command, CommandBody}; +use imap_codec::imap_types::core::AString; +use imap_codec::imap_types::response::Code; +use imap_codec::imap_types::secret::Secret; + +use aero_user::login::ArcLoginProvider; +use aero_collections::user::User; + +use crate::imap::capability::ServerCapability; +use crate::imap::command::anystate; +use crate::imap::flow; +use crate::imap::response::Response; + +//--- dispatching + +pub struct AnonymousContext<'a> { + pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, + pub login_provider: &'a ArcLoginProvider, +} + +pub async fn dispatch(ctx: AnonymousContext<'_>) -> Result<(Response<'static>, flow::Transition)> { + match &ctx.req.body { + // Any State + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } + CommandBody::Logout => anystate::logout(), + + // Specific to anonymous context (3 commands) + CommandBody::Login { username, password } => ctx.login(username, password).await, + CommandBody::Authenticate { .. } => { + anystate::not_implemented(ctx.req.tag.clone(), "authenticate") + } + //StartTLS is not implemented for now, we will probably go full TLS. + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), + } +} + +//--- Command controllers, private + +impl<'a> AnonymousContext<'a> { + async fn login( + self, + username: &AString<'a>, + password: &Secret>, + ) -> Result<(Response<'static>, flow::Transition)> { + let (u, p) = ( + std::str::from_utf8(username.as_ref())?, + std::str::from_utf8(password.declassify().as_ref())?, + ); + tracing::info!(user = %u, "command.login"); + + let creds = match self.login_provider.login(&u, &p).await { + Err(e) => { + tracing::debug!(error=%e, "authentication failed"); + return Ok(( + Response::build() + .to_req(self.req) + .message("Authentication failed") + .no()?, + flow::Transition::None, + )); + } + Ok(c) => c, + }; + + let user = User::new(u.to_string(), creds).await?; + + tracing::info!(username=%u, "connected"); + Ok(( + Response::build() + .to_req(self.req) + .code(Code::Capability(self.server_capabilities.to_vec())) + .message("Completed") + .ok()?, + flow::Transition::Authenticate(user), + )) + } +} diff --git a/aero-proto/src/imap/command/anystate.rs b/aero-proto/src/imap/command/anystate.rs new file mode 100644 index 0000000..718ba3f --- /dev/null +++ b/aero-proto/src/imap/command/anystate.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::response::Data; + +use crate::imap::capability::ServerCapability; +use crate::imap::flow; +use crate::imap::response::Response; + +pub(crate) fn capability( + tag: Tag<'static>, + cap: &ServerCapability, +) -> Result<(Response<'static>, flow::Transition)> { + let res = Response::build() + .tag(tag) + .message("Server capabilities") + .data(Data::Capability(cap.to_vec())) + .ok()?; + + Ok((res, flow::Transition::None)) +} + +pub(crate) fn noop_nothing(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build().tag(tag).message("Noop completed.").ok()?, + flow::Transition::None, + )) +} + +pub(crate) fn logout() -> Result<(Response<'static>, flow::Transition)> { + Ok((Response::bye()?, flow::Transition::Logout)) +} + +pub(crate) fn not_implemented<'a>( + tag: Tag<'a>, + what: &str, +) -> Result<(Response<'a>, flow::Transition)> { + Ok(( + Response::build() + .tag(tag) + .message(format!("Command not implemented {}", what)) + .bad()?, + flow::Transition::None, + )) +} + +pub(crate) fn wrong_state(tag: Tag<'static>) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .tag(tag) + .message("Command not authorized in this state") + .bad()?, + flow::Transition::None, + )) +} diff --git a/aero-proto/src/imap/command/authenticated.rs b/aero-proto/src/imap/command/authenticated.rs new file mode 100644 index 0000000..4c8d8c1 --- /dev/null +++ b/aero-proto/src/imap/command/authenticated.rs @@ -0,0 +1,682 @@ +use std::collections::BTreeMap; +use std::sync::Arc; +use thiserror::Error; + +use anyhow::{anyhow, bail, Result}; +use imap_codec::imap_types::command::{ + Command, CommandBody, ListReturnItem, SelectExamineModifier, +}; +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}; +use imap_codec::imap_types::mailbox::{ListMailbox, Mailbox as MailboxCodec}; +use imap_codec::imap_types::response::{Code, CodeOther, Data}; +use imap_codec::imap_types::status::{StatusDataItem, StatusDataItemName}; + +use aero_collections::mail::uidindex::*; +use aero_collections::user::User; +use aero_collections::mail::IMF; +use aero_collections::mail::namespace::MAILBOX_HIERARCHY_DELIMITER as MBX_HIER_DELIM_RAW; + +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::response::Response; + +pub struct AuthenticatedContext<'a> { + pub req: &'a Command<'static>, + pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, + pub user: &'a Arc, +} + +pub async fn dispatch<'a>( + mut ctx: AuthenticatedContext<'a>, +) -> Result<(Response<'static>, flow::Transition)> { + match &ctx.req.body { + // Any state + CommandBody::Noop => anystate::noop_nothing(ctx.req.tag.clone()), + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } + CommandBody::Logout => anystate::logout(), + + // Specific to this state (11 commands) + CommandBody::Create { mailbox } => ctx.create(mailbox).await, + CommandBody::Delete { mailbox } => ctx.delete(mailbox).await, + CommandBody::Rename { from, to } => ctx.rename(from, to).await, + CommandBody::Lsub { + reference, + mailbox_wildcard, + } => ctx.list(reference, mailbox_wildcard, &[], true).await, + CommandBody::List { + reference, + mailbox_wildcard, + r#return, + } => ctx.list(reference, mailbox_wildcard, r#return, false).await, + CommandBody::Status { + mailbox, + item_names, + } => ctx.status(mailbox, item_names).await, + CommandBody::Subscribe { mailbox } => ctx.subscribe(mailbox).await, + CommandBody::Unsubscribe { mailbox } => ctx.unsubscribe(mailbox).await, + CommandBody::Select { mailbox, modifiers } => ctx.select(mailbox, modifiers).await, + CommandBody::Examine { mailbox, modifiers } => ctx.examine(mailbox, modifiers).await, + CommandBody::Append { + mailbox, + flags, + date, + message, + } => ctx.append(mailbox, flags, date, message).await, + + // rfc5161 ENABLE + CommandBody::Enable { capabilities } => ctx.enable(capabilities), + + // Collect other commands + _ => anystate::wrong_state(ctx.req.tag.clone()), + } +} + +// --- PRIVATE --- +impl<'a> AuthenticatedContext<'a> { + async fn create( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let name = match mailbox { + MailboxCodec::Inbox => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Cannot create INBOX") + .bad()?, + flow::Transition::None, + )); + } + MailboxCodec::Other(aname) => std::str::from_utf8(aname.as_ref())?, + }; + + match self.user.create_mailbox(&name).await { + Ok(()) => Ok(( + Response::build() + .to_req(self.req) + .message("CREATE complete") + .ok()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::build() + .to_req(self.req) + .message(&e.to_string()) + .no()?, + flow::Transition::None, + )), + } + } + + async fn delete( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + match self.user.delete_mailbox(&name).await { + Ok(()) => Ok(( + Response::build() + .to_req(self.req) + .message("DELETE complete") + .ok()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::build() + .to_req(self.req) + .message(e.to_string()) + .no()?, + flow::Transition::None, + )), + } + } + + async fn rename( + self, + from: &MailboxCodec<'a>, + to: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(from).try_into()?; + let new_name: &str = MailboxName(to).try_into()?; + + match self.user.rename_mailbox(&name, &new_name).await { + Ok(()) => Ok(( + Response::build() + .to_req(self.req) + .message("RENAME complete") + .ok()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::build() + .to_req(self.req) + .message(e.to_string()) + .no()?, + flow::Transition::None, + )), + } + } + + async fn list( + &mut self, + reference: &MailboxCodec<'a>, + mailbox_wildcard: &ListMailbox<'a>, + must_return: &[ListReturnItem], + is_lsub: bool, + ) -> Result<(Response<'static>, flow::Transition)> { + let mbx_hier_delim: QuotedChar = QuotedChar::unvalidated(MBX_HIER_DELIM_RAW); + + let reference: &str = MailboxName(reference).try_into()?; + if !reference.is_empty() { + return Ok(( + Response::build() + .to_req(self.req) + .message("References not supported") + .bad()?, + flow::Transition::None, + )); + } + + let status_item_names = must_return.iter().find_map(|m| match m { + ListReturnItem::Status(v) => Some(v), + _ => None, + }); + + // @FIXME would probably need a rewrite to better use the imap_codec library + let wildcard = match mailbox_wildcard { + ListMailbox::Token(v) => std::str::from_utf8(v.as_ref())?, + ListMailbox::String(v) => std::str::from_utf8(v.as_ref())?, + }; + if wildcard.is_empty() { + if is_lsub { + return Ok(( + Response::build() + .to_req(self.req) + .message("LSUB complete") + .data(Data::Lsub { + items: vec![], + delimiter: Some(mbx_hier_delim), + mailbox: "".try_into().unwrap(), + }) + .ok()?, + flow::Transition::None, + )); + } else { + return Ok(( + Response::build() + .to_req(self.req) + .message("LIST complete") + .data(Data::List { + items: vec![], + delimiter: Some(mbx_hier_delim), + mailbox: "".try_into().unwrap(), + }) + .ok()?, + flow::Transition::None, + )); + } + } + + let mailboxes = self.user.list_mailboxes().await?; + let mut vmailboxes = BTreeMap::new(); + for mb in mailboxes.iter() { + for (i, _) in mb.match_indices(MBX_HIER_DELIM_RAW) { + if i > 0 { + let smb = &mb[..i]; + vmailboxes.entry(smb).or_insert(false); + } + } + vmailboxes.insert(mb, true); + } + + let mut ret = vec![]; + for (mb, is_real) in vmailboxes.iter() { + if matches_wildcard(&wildcard, mb) { + let mailbox: MailboxCodec = mb + .to_string() + .try_into() + .map_err(|_| anyhow!("invalid mailbox name"))?; + let mut items = vec![FlagNameAttribute::from(Atom::unvalidated("Subscribed"))]; + + // Decoration + if !*is_real { + items.push(FlagNameAttribute::Noselect); + } else { + match *mb { + "Drafts" => items.push(Atom::unvalidated("Drafts").into()), + "Archive" => items.push(Atom::unvalidated("Archive").into()), + "Sent" => items.push(Atom::unvalidated("Sent").into()), + "Trash" => items.push(Atom::unvalidated("Trash").into()), + _ => (), + }; + } + + // Result type + if is_lsub { + ret.push(Data::Lsub { + items, + delimiter: Some(mbx_hier_delim), + mailbox: mailbox.clone(), + }); + } else { + ret.push(Data::List { + items, + delimiter: Some(mbx_hier_delim), + mailbox: mailbox.clone(), + }); + } + + // Also collect status + if let Some(sin) = status_item_names { + let ret_attrs = match self.status_items(mb, sin).await { + Ok(a) => a, + Err(e) => { + tracing::error!(err=?e, mailbox=%mb, "Unable to fetch status for mailbox"); + continue; + } + }; + + let data = Data::Status { + mailbox, + items: ret_attrs.into(), + }; + + ret.push(data); + } + } + } + + let msg = if is_lsub { + "LSUB completed" + } else { + "LIST completed" + }; + Ok(( + Response::build() + .to_req(self.req) + .message(msg) + .many_data(ret) + .ok()?, + flow::Transition::None, + )) + } + + async fn status( + &mut self, + mailbox: &MailboxCodec<'static>, + attributes: &[StatusDataItemName], + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + let ret_attrs = match self.status_items(name, attributes).await { + Ok(v) => v, + Err(e) => match e.downcast_ref::() { + Some(CommandError::MailboxNotFound) => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Mailbox does not exist") + .no()?, + flow::Transition::None, + )) + } + _ => return Err(e.into()), + }, + }; + + let data = Data::Status { + mailbox: mailbox.clone(), + items: ret_attrs.into(), + }; + + Ok(( + Response::build() + .to_req(self.req) + .message("STATUS completed") + .data(data) + .ok()?, + flow::Transition::None, + )) + } + + async fn status_items( + &mut self, + name: &str, + attributes: &[StatusDataItemName], + ) -> Result> { + let mb_opt = self.user.open_mailbox(name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => return Err(CommandError::MailboxNotFound.into()), + }; + + let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; + + let mut ret_attrs = vec![]; + for attr in attributes.iter() { + ret_attrs.push(match attr { + StatusDataItemName::Messages => StatusDataItem::Messages(view.exists()?), + StatusDataItemName::Unseen => StatusDataItem::Unseen(view.unseen_count() as u32), + StatusDataItemName::Recent => StatusDataItem::Recent(view.recent()?), + StatusDataItemName::UidNext => StatusDataItem::UidNext(view.uidnext()), + StatusDataItemName::UidValidity => { + StatusDataItem::UidValidity(view.uidvalidity()) + } + StatusDataItemName::Deleted => { + bail!("quota not implemented, can't return deleted elements waiting for EXPUNGE"); + }, + StatusDataItemName::DeletedStorage => { + bail!("quota not implemented, can't return freed storage after EXPUNGE will be run"); + }, + StatusDataItemName::HighestModSeq => { + self.client_capabilities.enable_condstore(); + StatusDataItem::HighestModSeq(view.highestmodseq().get()) + }, + }); + } + Ok(ret_attrs) + } + + async fn subscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + if self.user.has_mailbox(&name).await? { + Ok(( + Response::build() + .to_req(self.req) + .message("SUBSCRIBE complete") + .ok()?, + flow::Transition::None, + )) + } else { + Ok(( + Response::build() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .bad()?, + flow::Transition::None, + )) + } + } + + async fn unsubscribe( + self, + mailbox: &MailboxCodec<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let name: &str = MailboxName(mailbox).try_into()?; + + if self.user.has_mailbox(&name).await? { + Ok(( + Response::build() + .to_req(self.req) + .message(format!( + "Cannot unsubscribe from mailbox {}: not supported by Aerogramme", + name + )) + .bad()?, + flow::Transition::None, + )) + } else { + Ok(( + Response::build() + .to_req(self.req) + .message(format!("Mailbox {} does not exist", name)) + .no()?, + flow::Transition::None, + )) + } + } + + /* + * TRACE BEGIN --- + + + Example: C: A142 SELECT INBOX + S: * 172 EXISTS + S: * 1 RECENT + S: * OK [UNSEEN 12] Message 12 is first unseen + S: * OK [UIDVALIDITY 3857529045] UIDs valid + S: * OK [UIDNEXT 4392] Predicted next UID + S: * FLAGS (\Answered \Flagged \Deleted \Seen \Draft) + S: * OK [PERMANENTFLAGS (\Deleted \Seen \*)] Limited + S: A142 OK [READ-WRITE] SELECT completed + + --- a mailbox with no unseen message -> no unseen entry + NOTES: + RFC3501 (imap4rev1) says if there is no OK [UNSEEN] response, client must make no assumption, + it is therefore correct to not return it even if there are unseen messages + RFC9051 (imap4rev2) says that OK [UNSEEN] responses are deprecated after SELECT and EXAMINE + For Aerogramme, we just don't send the OK [UNSEEN], it's correct to do in both specifications. + + + 20 select "INBOX.achats" + * FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1) + * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded JUNK $label1 \*)] Flags permitted. + * 88 EXISTS + * 0 RECENT + * OK [UIDVALIDITY 1347986788] UIDs valid + * OK [UIDNEXT 91] Predicted next UID + * OK [HIGHESTMODSEQ 72] Highest + 20 OK [READ-WRITE] Select completed (0.001 + 0.000 secs). + + * TRACE END --- + */ + async fn select( + self, + mailbox: &MailboxCodec<'a>, + modifiers: &[SelectExamineModifier], + ) -> Result<(Response<'static>, flow::Transition)> { + self.client_capabilities.select_enable(modifiers); + + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Mailbox does not exist") + .no()?, + flow::Transition::None, + )) + } + }; + tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.selected"); + + let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; + let data = mb.summary()?; + + Ok(( + Response::build() + .message("Select completed") + .to_req(self.req) + .code(Code::ReadWrite) + .set_body(data) + .ok()?, + flow::Transition::Select(mb, flow::MailboxPerm::ReadWrite), + )) + } + + async fn examine( + self, + mailbox: &MailboxCodec<'a>, + modifiers: &[SelectExamineModifier], + ) -> Result<(Response<'static>, flow::Transition)> { + self.client_capabilities.select_enable(modifiers); + + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Mailbox does not exist") + .no()?, + flow::Transition::None, + )) + } + }; + tracing::info!(username=%self.user.username, mailbox=%name, "mailbox.examined"); + + let mb = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; + let data = mb.summary()?; + + Ok(( + Response::build() + .to_req(self.req) + .message("Examine completed") + .code(Code::ReadOnly) + .set_body(data) + .ok()?, + flow::Transition::Select(mb, flow::MailboxPerm::ReadOnly), + )) + } + + //@FIXME we should write a specific version for the "selected" state + //that returns some unsollicited responses + async fn append( + self, + mailbox: &MailboxCodec<'a>, + flags: &[Flag<'a>], + date: &Option, + message: &Literal<'a>, + ) -> Result<(Response<'static>, flow::Transition)> { + let append_tag = self.req.tag.clone(); + match self.append_internal(mailbox, flags, date, message).await { + Ok((_mb_view, uidvalidity, uid, _modseq)) => Ok(( + Response::build() + .tag(append_tag) + .message("APPEND completed") + .code(Code::Other(CodeOther::unvalidated( + format!("APPENDUID {} {}", uidvalidity, uid).into_bytes(), + ))) + .ok()?, + flow::Transition::None, + )), + Err(e) => Ok(( + Response::build() + .tag(append_tag) + .message(e.to_string()) + .no()?, + flow::Transition::None, + )), + } + } + + fn enable( + self, + cap_enable: &Vec1>, + ) -> 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()); + if capabilities.len() > 0 { + response_builder = response_builder.data(Data::Enabled { capabilities }); + } + Ok(( + response_builder.message("ENABLE completed").ok()?, + flow::Transition::None, + )) + } + + //@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, + message: &Literal<'a>, + ) -> Result<(MailboxView, ImapUidvalidity, ImapUid, ModSeq)> { + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => bail!("Mailbox does not exist"), + }; + let view = MailboxView::new(mb, self.client_capabilities.condstore.is_enabled()).await; + + if date.is_some() { + tracing::warn!("Cannot set date when appending message"); + } + + let msg = + IMF::try_from(message.data()).map_err(|_| anyhow!("Could not parse e-mail message"))?; + let flags = flags.iter().map(|x| x.to_string()).collect::>(); + // TODO: filter allowed flags? ping @Quentin + + let (uidvalidity, uid, modseq) = + view.internal.mailbox.append(msg, None, &flags[..]).await?; + //let unsollicited = view.update(UpdateParameters::default()).await?; + + Ok((view, uidvalidity, uid, modseq)) + } +} + +fn matches_wildcard(wildcard: &str, name: &str) -> bool { + let wildcard = wildcard.chars().collect::>(); + let name = name.chars().collect::>(); + + let mut matches = vec![vec![false; wildcard.len() + 1]; name.len() + 1]; + + for i in 0..=name.len() { + for j in 0..=wildcard.len() { + matches[i][j] = (i == 0 && j == 0) + || (j > 0 + && matches[i][j - 1] + && (wildcard[j - 1] == '%' || wildcard[j - 1] == '*')) + || (i > 0 + && j > 0 + && matches[i - 1][j - 1] + && wildcard[j - 1] == name[i - 1] + && wildcard[j - 1] != '%' + && wildcard[j - 1] != '*') + || (i > 0 + && j > 0 + && matches[i - 1][j] + && (wildcard[j - 1] == '*' + || (wildcard[j - 1] == '%' && name[i - 1] != MBX_HIER_DELIM_RAW))); + } + } + + matches[name.len()][wildcard.len()] +} + +#[derive(Error, Debug)] +pub enum CommandError { + #[error("Mailbox not found")] + MailboxNotFound, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wildcard_matches() { + assert!(matches_wildcard("INBOX", "INBOX")); + assert!(matches_wildcard("*", "INBOX")); + assert!(matches_wildcard("%", "INBOX")); + assert!(!matches_wildcard("%", "Test.Azerty")); + assert!(!matches_wildcard("INBOX.*", "INBOX")); + assert!(matches_wildcard("Sent.*", "Sent.A")); + assert!(matches_wildcard("Sent.*", "Sent.A.B")); + assert!(!matches_wildcard("Sent.%", "Sent.A.B")); + } +} diff --git a/aero-proto/src/imap/command/mod.rs b/aero-proto/src/imap/command/mod.rs new file mode 100644 index 0000000..5382d06 --- /dev/null +++ b/aero-proto/src/imap/command/mod.rs @@ -0,0 +1,20 @@ +pub mod anonymous; +pub mod anystate; +pub mod authenticated; +pub mod selected; + +use aero_collections::mail::namespace::INBOX; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; + +/// Convert an IMAP mailbox name/identifier representation +/// to an utf-8 string that is used internally in Aerogramme +struct MailboxName<'a>(&'a MailboxCodec<'a>); +impl<'a> TryInto<&'a str> for MailboxName<'a> { + type Error = std::str::Utf8Error; + fn try_into(self) -> Result<&'a str, Self::Error> { + match self.0 { + MailboxCodec::Inbox => Ok(INBOX), + MailboxCodec::Other(aname) => Ok(std::str::from_utf8(aname.as_ref())?), + } + } +} diff --git a/aero-proto/src/imap/command/selected.rs b/aero-proto/src/imap/command/selected.rs new file mode 100644 index 0000000..190949b --- /dev/null +++ b/aero-proto/src/imap/command/selected.rs @@ -0,0 +1,425 @@ +use std::num::NonZeroU64; +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::fetch::MacroOrMessageDataItemNames; +use imap_codec::imap_types::flag::{Flag, StoreResponse, StoreType}; +use imap_codec::imap_types::mailbox::Mailbox as MailboxCodec; +use imap_codec::imap_types::response::{Code, CodeOther}; +use imap_codec::imap_types::search::SearchKey; +use imap_codec::imap_types::sequence::SequenceSet; + +use aero_collections::user::User; + +use crate::imap::attributes::AttributesProxy; +use crate::imap::capability::{ClientCapability, ServerCapability}; +use crate::imap::command::{anystate, authenticated, MailboxName}; +use crate::imap::flow; +use crate::imap::mailbox_view::{MailboxView, UpdateParameters}; +use crate::imap::response::Response; + +pub struct SelectedContext<'a> { + pub req: &'a Command<'static>, + pub user: &'a Arc, + pub mailbox: &'a mut MailboxView, + pub server_capabilities: &'a ServerCapability, + pub client_capabilities: &'a mut ClientCapability, + pub perm: &'a flow::MailboxPerm, +} + +pub async fn dispatch<'a>( + ctx: SelectedContext<'a>, +) -> Result<(Response<'static>, flow::Transition)> { + match &ctx.req.body { + // Any State + // noop is specific to this state + CommandBody::Capability => { + anystate::capability(ctx.req.tag.clone(), ctx.server_capabilities) + } + CommandBody::Logout => anystate::logout(), + + // Specific to this state (7 commands + NOOP) + CommandBody::Close => match ctx.perm { + flow::MailboxPerm::ReadWrite => ctx.close().await, + flow::MailboxPerm::ReadOnly => ctx.examine_close().await, + }, + CommandBody::Noop | CommandBody::Check => ctx.noop().await, + CommandBody::Fetch { + sequence_set, + macro_or_item_names, + modifiers, + uid, + } => { + 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, &SearchKey::And(criteria.clone()), uid) + .await + } + CommandBody::Expunge { + // UIDPLUS (rfc4315) + uid_sequence_set, + } => ctx.expunge(uid_sequence_set).await, + CommandBody::Store { + sequence_set, + kind, + response, + flags, + modifiers, + uid, + } => { + ctx.store(sequence_set, kind, response, flags, modifiers, uid) + .await + } + CommandBody::Copy { + sequence_set, + mailbox, + uid, + } => ctx.copy(sequence_set, mailbox, uid).await, + CommandBody::Move { + sequence_set, + mailbox, + uid, + } => ctx.r#move(sequence_set, mailbox, uid).await, + + // UNSELECT extension (rfc3691) + CommandBody::Unselect => ctx.unselect().await, + + // In selected mode, we fallback to authenticated when needed + _ => { + authenticated::dispatch(authenticated::AuthenticatedContext { + req: ctx.req, + server_capabilities: ctx.server_capabilities, + client_capabilities: ctx.client_capabilities, + user: ctx.user, + }) + .await + } + } +} + +// --- PRIVATE --- + +impl<'a> SelectedContext<'a> { + async fn close(self) -> Result<(Response<'static>, flow::Transition)> { + // We expunge messages, + // but we don't send the untagged EXPUNGE responses + let tag = self.req.tag.clone(); + self.expunge(&None).await?; + Ok(( + Response::build().tag(tag).message("CLOSE completed").ok()?, + flow::Transition::Unselect, + )) + } + + /// CLOSE in examined state is not the same as in selected state + /// (in selected state it also does an EXPUNGE, here it doesn't) + async fn examine_close(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("CLOSE completed") + .ok()?, + flow::Transition::Unselect, + )) + } + + async fn unselect(self) -> Result<(Response<'static>, flow::Transition)> { + Ok(( + Response::build() + .to_req(self.req) + .message("UNSELECT completed") + .ok()?, + flow::Transition::Unselect, + )) + } + + pub async fn fetch( + self, + sequence_set: &SequenceSet, + attributes: &'a MacroOrMessageDataItemNames<'static>, + modifiers: &[FetchModifier], + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + let ap = AttributesProxy::new(attributes, modifiers, *uid); + let mut changed_since: Option = None; + modifiers.iter().for_each(|m| match m { + FetchModifier::ChangedSince(val) => { + changed_since = Some(*val); + } + }); + + match self + .mailbox + .fetch(sequence_set, &ap, changed_since, uid) + .await + { + Ok(resp) => { + // Capabilities enabling logic only on successful command + // (according to my understanding of the spec) + self.client_capabilities.attributes_enable(&ap); + self.client_capabilities.fetch_modifiers_enable(modifiers); + + // Response to the client + Ok(( + Response::build() + .to_req(self.req) + .message("FETCH completed") + .set_body(resp) + .ok()?, + flow::Transition::None, + )) + } + Err(e) => Ok(( + Response::build() + .to_req(self.req) + .message(e.to_string()) + .no()?, + flow::Transition::None, + )), + } + } + + pub async fn search( + self, + charset: &Option>, + criteria: &SearchKey<'a>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + let (found, enable_condstore) = self.mailbox.search(charset, criteria, *uid).await?; + if enable_condstore { + self.client_capabilities.enable_condstore(); + } + Ok(( + Response::build() + .to_req(self.req) + .set_body(found) + .message("SEARCH completed") + .ok()?, + flow::Transition::None, + )) + } + + pub async fn noop(self) -> Result<(Response<'static>, flow::Transition)> { + self.mailbox.internal.mailbox.force_sync().await?; + + let updates = self.mailbox.update(UpdateParameters::default()).await?; + Ok(( + Response::build() + .to_req(self.req) + .message("NOOP completed.") + .set_body(updates) + .ok()?, + flow::Transition::None, + )) + } + + async fn expunge( + self, + uid_sequence_set: &Option, + ) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + + let tag = self.req.tag.clone(); + let data = self.mailbox.expunge(uid_sequence_set).await?; + + Ok(( + Response::build() + .tag(tag) + .message("EXPUNGE completed") + .set_body(data) + .ok()?, + flow::Transition::None, + )) + } + + async fn store( + self, + sequence_set: &SequenceSet, + kind: &StoreType, + response: &StoreResponse, + flags: &[Flag<'a>], + modifiers: &[StoreModifier], + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + + let mut unchanged_since: Option = None; + modifiers.iter().for_each(|m| match m { + StoreModifier::UnchangedSince(val) => { + unchanged_since = Some(*val); + } + }); + + let (data, modified) = self + .mailbox + .store(sequence_set, kind, response, flags, unchanged_since, uid) + .await?; + + let mut ok_resp = Response::build() + .to_req(self.req) + .message("STORE completed") + .set_body(data); + + match modified[..] { + [] => (), + [_head, ..] => { + let modified_str = format!( + "MODIFIED {}", + modified + .into_iter() + .map(|x| x.to_string()) + .collect::>() + .join(",") + ); + ok_resp = ok_resp.code(Code::Other(CodeOther::unvalidated( + modified_str.into_bytes(), + ))); + } + }; + + self.client_capabilities.store_modifiers_enable(modifiers); + + Ok((ok_resp.ok()?, flow::Transition::None)) + } + + async fn copy( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec<'a>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + //@FIXME Could copy be valid in EXAMINE mode? + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, + flow::Transition::None, + )) + } + }; + + let (uidval, uid_map) = self.mailbox.copy(sequence_set, mb, uid).await?; + + let copyuid_str = format!( + "{} {} {}", + uidval, + uid_map + .iter() + .map(|(sid, _)| format!("{}", sid)) + .collect::>() + .join(","), + uid_map + .iter() + .map(|(_, tuid)| format!("{}", tuid)) + .collect::>() + .join(",") + ); + + Ok(( + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .ok()?, + flow::Transition::None, + )) + } + + async fn r#move( + self, + sequence_set: &SequenceSet, + mailbox: &MailboxCodec<'a>, + uid: &bool, + ) -> Result<(Response<'static>, flow::Transition)> { + if let Some(failed) = self.fail_read_only() { + return Ok((failed, flow::Transition::None)); + } + + let name: &str = MailboxName(mailbox).try_into()?; + + let mb_opt = self.user.open_mailbox(&name).await?; + let mb = match mb_opt { + Some(mb) => mb, + None => { + return Ok(( + Response::build() + .to_req(self.req) + .message("Destination mailbox does not exist") + .code(Code::TryCreate) + .no()?, + flow::Transition::None, + )) + } + }; + + let (uidval, uid_map, data) = self.mailbox.r#move(sequence_set, mb, uid).await?; + + // compute code + let copyuid_str = format!( + "{} {} {}", + uidval, + uid_map + .iter() + .map(|(sid, _)| format!("{}", sid)) + .collect::>() + .join(","), + uid_map + .iter() + .map(|(_, tuid)| format!("{}", tuid)) + .collect::>() + .join(",") + ); + + Ok(( + Response::build() + .to_req(self.req) + .message("COPY completed") + .code(Code::Other(CodeOther::unvalidated( + format!("COPYUID {}", copyuid_str).into_bytes(), + ))) + .set_body(data) + .ok()?, + flow::Transition::None, + )) + } + + fn fail_read_only(&self) -> Option> { + match self.perm { + flow::MailboxPerm::ReadWrite => None, + flow::MailboxPerm::ReadOnly => Some( + Response::build() + .to_req(self.req) + .message("Write command are forbidden while exmining mailbox") + .no() + .unwrap(), + ), + } + } +} diff --git a/aero-proto/src/imap/flags.rs b/aero-proto/src/imap/flags.rs new file mode 100644 index 0000000..0f6ec64 --- /dev/null +++ b/aero-proto/src/imap/flags.rs @@ -0,0 +1,30 @@ +use imap_codec::imap_types::core::Atom; +use imap_codec::imap_types::flag::{Flag, FlagFetch}; + +pub fn from_str(f: &str) -> Option> { + match f.chars().next() { + Some('\\') => match f { + "\\Seen" => Some(FlagFetch::Flag(Flag::Seen)), + "\\Answered" => Some(FlagFetch::Flag(Flag::Answered)), + "\\Flagged" => Some(FlagFetch::Flag(Flag::Flagged)), + "\\Deleted" => Some(FlagFetch::Flag(Flag::Deleted)), + "\\Draft" => Some(FlagFetch::Flag(Flag::Draft)), + "\\Recent" => Some(FlagFetch::Recent), + _ => match Atom::try_from(f.strip_prefix('\\').unwrap().to_string()) { + Err(_) => { + tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); + None + } + Ok(a) => Some(FlagFetch::Flag(Flag::system(a))), + }, + }, + Some(_) => match Atom::try_from(f.to_string()) { + Err(_) => { + tracing::error!(flag=%f, "Unable to encode flag as IMAP atom"); + None + } + Ok(a) => Some(FlagFetch::Flag(Flag::keyword(a))), + }, + None => None, + } +} diff --git a/aero-proto/src/imap/flow.rs b/aero-proto/src/imap/flow.rs new file mode 100644 index 0000000..1986447 --- /dev/null +++ b/aero-proto/src/imap/flow.rs @@ -0,0 +1,115 @@ +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 aero_collections::user::User; + +use crate::imap::mailbox_view::MailboxView; + +#[derive(Debug)] +pub enum Error { + ForbiddenTransition, +} +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Forbidden Transition") + } +} +impl StdError for Error {} + +pub enum State { + NotAuthenticated, + Authenticated(Arc), + Selected(Arc, MailboxView, MailboxPerm), + Idle( + Arc, + MailboxView, + MailboxPerm, + Tag<'static>, + Arc, + ), + Logout, +} +impl State { + pub fn notify(&self) -> Option> { + 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::*; + match self { + NotAuthenticated => write!(f, "NotAuthenticated"), + Authenticated(..) => write!(f, "Authenticated"), + Selected(..) => write!(f, "Selected"), + Idle(..) => write!(f, "Idle"), + Logout => write!(f, "Logout"), + } + } +} + +#[derive(Clone)] +pub enum MailboxPerm { + ReadOnly, + ReadWrite, +} + +pub enum Transition { + None, + Authenticate(Arc), + Select(MailboxView, MailboxPerm), + Idle(Tag<'static>, Notify), + UnIdle, + Unselect, + Logout, +} +impl fmt::Display for Transition { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Transition::*; + match self { + None => write!(f, "None"), + Authenticate(..) => write!(f, "Authenticated"), + Select(..) => write!(f, "Selected"), + Idle(..) => write!(f, "Idle"), + UnIdle => write!(f, "UnIdle"), + Unselect => write!(f, "Unselect"), + Logout => write!(f, "Logout"), + } + } +} + +// See RFC3501 section 3. +// https://datatracker.ietf.org/doc/html/rfc3501#page-13 +impl State { + pub fn apply(&mut self, tr: Transition) -> Result<(), Error> { + tracing::debug!(state=%self, transition=%tr, "try change state"); + + let new_state = match (std::mem::replace(self, State::Logout), tr) { + (s, Transition::None) => s, + (State::NotAuthenticated, Transition::Authenticate(u)) => State::Authenticated(u), + (State::Authenticated(u) | State::Selected(u, _, _), Transition::Select(m, p)) => { + State::Selected(u, m, p) + } + (State::Selected(u, _, _), Transition::Unselect) => State::Authenticated(u.clone()), + (State::Selected(u, m, p), Transition::Idle(t, s)) => { + State::Idle(u, m, p, t, Arc::new(s)) + } + (State::Idle(u, m, p, _, _), Transition::UnIdle) => State::Selected(u, m, p), + (_, Transition::Logout) => State::Logout, + (s, t) => { + tracing::error!(state=%s, transition=%t, "forbidden transition"); + return Err(Error::ForbiddenTransition); + } + }; + *self = new_state; + tracing::debug!(state=%self, "transition succeeded"); + + Ok(()) + } +} diff --git a/aero-proto/src/imap/imf_view.rs b/aero-proto/src/imap/imf_view.rs new file mode 100644 index 0000000..a4ca2e8 --- /dev/null +++ b/aero-proto/src/imap/imf_view.rs @@ -0,0 +1,109 @@ +use anyhow::{anyhow, Result}; +use chrono::naive::NaiveDate; + +use imap_codec::imap_types::core::{IString, NString}; +use imap_codec::imap_types::envelope::{Address, Envelope}; + +use eml_codec::imf; + +pub struct ImfView<'a>(pub &'a imf::Imf<'a>); + +impl<'a> ImfView<'a> { + pub fn naive_date(&self) -> Result { + Ok(self.0.date.ok_or(anyhow!("date is not set"))?.date_naive()) + } + + /// Envelope rules are defined in RFC 3501, section 7.4.2 + /// https://datatracker.ietf.org/doc/html/rfc3501#section-7.4.2 + /// + /// Some important notes: + /// + /// If the Sender or Reply-To lines are absent in the [RFC-2822] + /// header, or are present but empty, the server sets the + /// corresponding member of the envelope to be the same value as + /// the from member (the client is not expected to know to do + /// this). Note: [RFC-2822] requires that all messages have a valid + /// From header. Therefore, the from, sender, and reply-to + /// members in the envelope can not be NIL. + /// + /// If the Date, Subject, In-Reply-To, and Message-ID header lines + /// are absent in the [RFC-2822] header, the corresponding member + /// of the envelope is NIL; if these header lines are present but + /// empty the corresponding member of the envelope is the empty + /// string. + + //@FIXME return an error if the envelope is invalid instead of panicking + //@FIXME some fields must be defaulted if there are not set. + pub fn message_envelope(&self) -> Envelope<'static> { + let msg = self.0; + let from = msg.from.iter().map(convert_mbx).collect::>(); + + Envelope { + date: NString( + msg.date + .as_ref() + .map(|d| IString::try_from(d.to_rfc3339()).unwrap()), + ), + subject: NString( + msg.subject + .as_ref() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + sender: msg + .sender + .as_ref() + .map(|v| vec![convert_mbx(v)]) + .unwrap_or(from.clone()), + reply_to: if msg.reply_to.is_empty() { + from.clone() + } else { + convert_addresses(&msg.reply_to) + }, + from, + to: convert_addresses(&msg.to), + cc: convert_addresses(&msg.cc), + bcc: convert_addresses(&msg.bcc), + in_reply_to: NString( + msg.in_reply_to + .iter() + .next() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + message_id: NString( + msg.msg_id + .as_ref() + .map(|d| IString::try_from(d.to_string()).unwrap()), + ), + } + } +} + +pub fn convert_addresses(addrlist: &Vec) -> Vec> { + let mut acc = vec![]; + for item in addrlist { + match item { + imf::address::AddressRef::Single(a) => acc.push(convert_mbx(a)), + imf::address::AddressRef::Many(l) => acc.extend(l.participants.iter().map(convert_mbx)), + } + } + return acc; +} + +pub fn convert_mbx(addr: &imf::mailbox::MailboxRef) -> Address<'static> { + Address { + name: NString( + addr.name + .as_ref() + .map(|x| IString::try_from(x.to_string()).unwrap()), + ), + // SMTP at-domain-list (source route) seems obsolete since at least 1991 + // https://www.mhonarc.org/archive/html/ietf-822/1991-06/msg00060.html + adl: NString(None), + mailbox: NString(Some( + IString::try_from(addr.addrspec.local_part.to_string()).unwrap(), + )), + host: NString(Some( + IString::try_from(addr.addrspec.domain.to_string()).unwrap(), + )), + } +} diff --git a/aero-proto/src/imap/index.rs b/aero-proto/src/imap/index.rs new file mode 100644 index 0000000..3de46be --- /dev/null +++ b/aero-proto/src/imap/index.rs @@ -0,0 +1,211 @@ +use std::num::{NonZeroU32, NonZeroU64}; + +use anyhow::{anyhow, Result}; +use imap_codec::imap_types::sequence::{SeqOrUid, Sequence, SequenceSet}; + +use aero_collections::mail::uidindex::{ImapUid, ModSeq, UidIndex}; +use aero_collections::mail::unique_ident::UniqueIdent; + +pub struct Index<'a> { + pub imap_index: Vec>, + pub internal: &'a UidIndex, +} +impl<'a> Index<'a> { + pub fn new(internal: &'a UidIndex) -> Result { + let imap_index = internal + .idx_by_uid + .iter() + .enumerate() + .map(|(i_enum, (&uid, &uuid))| { + let (_, modseq, flags) = internal + .table + .get(&uuid) + .ok_or(anyhow!("mail is missing from index"))?; + let i_int: u32 = (i_enum + 1).try_into()?; + let i: NonZeroU32 = i_int.try_into()?; + + Ok(MailIndex { + i, + uid, + uuid, + modseq: *modseq, + flags, + }) + }) + .collect::>>()?; + + Ok(Self { + imap_index, + internal, + }) + } + + pub fn last(&'a self) -> Option<&'a MailIndex<'a>> { + self.imap_index.last() + } + + /// Fetch mail descriptors based on a sequence of UID + /// + /// Complexity analysis: + /// - Sort is O(n * log n) where n is the number of uid generated by the sequence + /// - Finding the starting point in the index O(log m) where m is the size of the mailbox + /// While n =< m, it's not clear if the difference is big or not. + /// + /// For now, the algorithm tries to be fast for small values of n, + /// as it is what is expected by clients. + /// + /// So we assume for our implementation that : n << m. + /// It's not true for full mailbox searches for example... + pub fn fetch_on_uid(&'a self, sequence_set: &SequenceSet) -> Vec<&'a MailIndex<'a>> { + if self.imap_index.is_empty() { + return vec![]; + } + let largest = self.last().expect("The mailbox is not empty").uid; + let mut unroll_seq = sequence_set.iter(largest).collect::>(); + unroll_seq.sort(); + + let start_seq = match unroll_seq.iter().next() { + Some(elem) => elem, + None => return vec![], + }; + + // Quickly jump to the right point in the mailbox vector O(log m) instead + // of iterating one by one O(m). Works only because both unroll_seq & imap_index are sorted per uid. + let mut imap_idx = { + let start_idx = self + .imap_index + .partition_point(|mail_idx| &mail_idx.uid < start_seq); + &self.imap_index[start_idx..] + }; + + let mut acc = vec![]; + for wanted_uid in unroll_seq.iter() { + // Slide the window forward as long as its first element is lower than our wanted uid. + let start_idx = match imap_idx.iter().position(|midx| &midx.uid >= wanted_uid) { + Some(v) => v, + None => break, + }; + imap_idx = &imap_idx[start_idx..]; + + // If the beginning of our new window is the uid we want, we collect it + if &imap_idx[0].uid == wanted_uid { + acc.push(&imap_idx[0]); + } + } + + acc + } + + pub fn fetch_on_id(&'a self, sequence_set: &SequenceSet) -> Result>> { + if self.imap_index.is_empty() { + return Ok(vec![]); + } + let largest = NonZeroU32::try_from(self.imap_index.len() as u32)?; + let mut acc = sequence_set + .iter(largest) + .map(|wanted_id| { + self.imap_index + .get((wanted_id.get() as usize) - 1) + .ok_or(anyhow!("Mail not found")) + }) + .collect::>>()?; + + // Sort the result to be consistent with UID + acc.sort_by(|a, b| a.i.cmp(&b.i)); + + Ok(acc) + } + + pub fn fetch( + self: &'a Index<'a>, + sequence_set: &SequenceSet, + by_uid: bool, + ) -> Result>> { + match by_uid { + true => Ok(self.fetch_on_uid(sequence_set)), + _ => self.fetch_on_id(sequence_set), + } + } + + pub fn fetch_changed_since( + self: &'a Index<'a>, + sequence_set: &SequenceSet, + maybe_modseq: Option, + by_uid: bool, + ) -> Result>> { + let raw = self.fetch(sequence_set, by_uid)?; + let res = match maybe_modseq { + Some(pit) => raw.into_iter().filter(|midx| midx.modseq > pit).collect(), + None => raw, + }; + + Ok(res) + } + + pub fn fetch_unchanged_since( + self: &'a Index<'a>, + sequence_set: &SequenceSet, + maybe_modseq: Option, + by_uid: bool, + ) -> Result<(Vec<&'a MailIndex<'a>>, Vec<&'a MailIndex<'a>>)> { + let raw = self.fetch(sequence_set, by_uid)?; + let res = match maybe_modseq { + Some(pit) => raw.into_iter().partition(|midx| midx.modseq <= pit), + None => (raw, vec![]), + }; + + Ok(res) + } +} + +#[derive(Clone, Debug)] +pub struct MailIndex<'a> { + pub i: NonZeroU32, + pub uid: ImapUid, + pub uuid: UniqueIdent, + pub modseq: ModSeq, + pub flags: &'a Vec, +} + +impl<'a> MailIndex<'a> { + // The following functions are used to implement the SEARCH command + pub fn is_in_sequence_i(&self, seq: &Sequence) -> bool { + match seq { + Sequence::Single(SeqOrUid::Asterisk) => true, + Sequence::Single(SeqOrUid::Value(target)) => target == &self.i, + Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) + | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.i, + Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { + if x1 < x2 { + x1 <= &self.i && &self.i <= x2 + } else { + x1 >= &self.i && &self.i >= x2 + } + } + Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, + } + } + + pub fn is_in_sequence_uid(&self, seq: &Sequence) -> bool { + match seq { + Sequence::Single(SeqOrUid::Asterisk) => true, + Sequence::Single(SeqOrUid::Value(target)) => target == &self.uid, + Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Value(x)) + | Sequence::Range(SeqOrUid::Value(x), SeqOrUid::Asterisk) => x <= &self.uid, + Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { + if x1 < x2 { + x1 <= &self.uid && &self.uid <= x2 + } else { + x1 >= &self.uid && &self.uid >= x2 + } + } + Sequence::Range(SeqOrUid::Asterisk, SeqOrUid::Asterisk) => true, + } + } + + pub fn is_flag_set(&self, flag: &str) -> bool { + self.flags + .iter() + .any(|candidate| candidate.as_str() == flag) + } +} diff --git a/aero-proto/src/imap/mail_view.rs b/aero-proto/src/imap/mail_view.rs new file mode 100644 index 0000000..054014a --- /dev/null +++ b/aero-proto/src/imap/mail_view.rs @@ -0,0 +1,306 @@ +use std::num::NonZeroU32; + +use anyhow::{anyhow, bail, Result}; +use chrono::{naive::NaiveDate, DateTime as ChronoDateTime, Local, Offset, TimeZone, Utc}; + +use imap_codec::imap_types::core::NString; +use imap_codec::imap_types::datetime::DateTime; +use imap_codec::imap_types::fetch::{ + MessageDataItem, MessageDataItemName, Section as FetchSection, +}; +use imap_codec::imap_types::flag::Flag; +use imap_codec::imap_types::response::Data; + +use eml_codec::{ + imf, + part::{composite::Message, AnyPart}, +}; + +use aero_collections::mail::query::QueryResult; + +use crate::imap::attributes::AttributesProxy; +use crate::imap::flags; +use crate::imap::imf_view::ImfView; +use crate::imap::index::MailIndex; +use crate::imap::mime_view; +use crate::imap::response::Body; + +pub struct MailView<'a> { + pub in_idx: &'a MailIndex<'a>, + pub query_result: &'a QueryResult, + pub content: FetchedMail<'a>, +} + +impl<'a> MailView<'a> { + pub fn new(query_result: &'a QueryResult, in_idx: &'a MailIndex<'a>) -> Result> { + Ok(Self { + in_idx, + query_result, + content: match query_result { + QueryResult::FullResult { content, .. } => { + let (_, parsed) = + eml_codec::parse_message(&content).or(Err(anyhow!("Invalid mail body")))?; + FetchedMail::full_from_message(parsed) + } + QueryResult::PartialResult { metadata, .. } => { + let (_, parsed) = eml_codec::parse_message(&metadata.headers) + .or(Err(anyhow!("unable to parse email headers")))?; + FetchedMail::partial_from_message(parsed) + } + QueryResult::IndexResult { .. } => FetchedMail::IndexOnly, + }, + }) + } + + pub fn imf(&self) -> Option { + self.content.as_imf().map(ImfView) + } + + pub fn selected_mime(&'a self) -> Option> { + self.content.as_anypart().ok().map(mime_view::SelectedMime) + } + + pub fn filter(&self, ap: &AttributesProxy) -> Result<(Body<'static>, SeenFlag)> { + let mut seen = SeenFlag::DoNothing; + let res_attrs = ap + .attrs + .iter() + .map(|attr| match attr { + MessageDataItemName::Uid => Ok(self.uid()), + MessageDataItemName::Flags => Ok(self.flags()), + MessageDataItemName::Rfc822Size => self.rfc_822_size(), + MessageDataItemName::Rfc822Header => self.rfc_822_header(), + MessageDataItemName::Rfc822Text => self.rfc_822_text(), + MessageDataItemName::Rfc822 => { + if self.is_not_yet_seen() { + seen = SeenFlag::MustAdd; + } + self.rfc822() + } + MessageDataItemName::Envelope => Ok(self.envelope()), + MessageDataItemName::Body => self.body(), + MessageDataItemName::BodyStructure => self.body_structure(), + MessageDataItemName::BodyExt { + section, + partial, + peek, + } => { + let (body, has_seen) = self.body_ext(section, partial, peek)?; + seen = has_seen; + Ok(body) + } + MessageDataItemName::InternalDate => self.internal_date(), + MessageDataItemName::ModSeq => Ok(self.modseq()), + }) + .collect::, _>>()?; + + Ok(( + Body::Data(Data::Fetch { + seq: self.in_idx.i, + items: res_attrs.try_into()?, + }), + seen, + )) + } + + pub fn stored_naive_date(&self) -> Result { + let mail_meta = self.query_result.metadata().expect("metadata were fetched"); + let mail_ts: i64 = mail_meta.internaldate.try_into()?; + let msg_date: ChronoDateTime = ChronoDateTime::from_timestamp(mail_ts, 0) + .ok_or(anyhow!("unable to parse timestamp"))? + .with_timezone(&Local); + + Ok(msg_date.date_naive()) + } + + pub fn is_header_contains_pattern(&self, hdr: &[u8], pattern: &[u8]) -> bool { + let mime = match self.selected_mime() { + None => return false, + Some(x) => x, + }; + + let val = match mime.header_value(hdr) { + None => return false, + Some(x) => x, + }; + + val.windows(pattern.len()).any(|win| win == pattern) + } + + // Private function, mainly for filter! + fn uid(&self) -> MessageDataItem<'static> { + MessageDataItem::Uid(self.in_idx.uid.clone()) + } + + fn flags(&self) -> MessageDataItem<'static> { + MessageDataItem::Flags( + self.in_idx + .flags + .iter() + .filter_map(|f| flags::from_str(f)) + .collect(), + ) + } + + fn rfc_822_size(&self) -> Result> { + let sz = self + .query_result + .metadata() + .ok_or(anyhow!("mail metadata are required"))? + .rfc822_size; + Ok(MessageDataItem::Rfc822Size(sz as u32)) + } + + fn rfc_822_header(&self) -> Result> { + let hdrs: NString = self + .query_result + .metadata() + .ok_or(anyhow!("mail metadata are required"))? + .headers + .to_vec() + .try_into()?; + Ok(MessageDataItem::Rfc822Header(hdrs)) + } + + fn rfc_822_text(&self) -> Result> { + let txt: NString = self.content.as_msg()?.raw_body.to_vec().try_into()?; + Ok(MessageDataItem::Rfc822Text(txt)) + } + + fn rfc822(&self) -> Result> { + let full: NString = self.content.as_msg()?.raw_part.to_vec().try_into()?; + Ok(MessageDataItem::Rfc822(full)) + } + + fn envelope(&self) -> MessageDataItem<'static> { + MessageDataItem::Envelope( + self.imf() + .expect("an imf object is derivable from fetchedmail") + .message_envelope(), + ) + } + + fn body(&self) -> Result> { + Ok(MessageDataItem::Body(mime_view::bodystructure( + self.content.as_msg()?.child.as_ref(), + false, + )?)) + } + + fn body_structure(&self) -> Result> { + Ok(MessageDataItem::BodyStructure(mime_view::bodystructure( + self.content.as_msg()?.child.as_ref(), + true, + )?)) + } + + fn is_not_yet_seen(&self) -> bool { + let seen_flag = Flag::Seen.to_string(); + !self.in_idx.flags.iter().any(|x| *x == seen_flag) + } + + /// maps to BODY[
]<> and BODY.PEEK[
]<> + /// peek does not implicitly set the \Seen flag + /// eg. BODY[HEADER.FIELDS (DATE FROM)] + /// eg. BODY[]<0.2048> + fn body_ext( + &self, + section: &Option>, + partial: &Option<(u32, NonZeroU32)>, + peek: &bool, + ) -> Result<(MessageDataItem<'static>, SeenFlag)> { + // Manage Seen flag + let mut seen = SeenFlag::DoNothing; + if !peek && self.is_not_yet_seen() { + // Add \Seen flag + //self.mailbox.add_flags(uuid, &[seen_flag]).await?; + seen = SeenFlag::MustAdd; + } + + // Process message + let (text, origin) = + match mime_view::body_ext(self.content.as_anypart()?, section, partial)? { + mime_view::BodySection::Full(body) => (body, None), + mime_view::BodySection::Slice { body, origin_octet } => (body, Some(origin_octet)), + }; + + let data: NString = text.to_vec().try_into()?; + + return Ok(( + MessageDataItem::BodyExt { + section: section.as_ref().map(|fs| fs.clone()), + origin, + data, + }, + seen, + )); + } + + fn internal_date(&self) -> Result> { + let dt = Utc + .fix() + .timestamp_opt( + i64::try_from( + self.query_result + .metadata() + .ok_or(anyhow!("mail metadata were not fetched"))? + .internaldate + / 1000, + )?, + 0, + ) + .earliest() + .ok_or(anyhow!("Unable to parse internal date"))?; + Ok(MessageDataItem::InternalDate(DateTime::unvalidated(dt))) + } + + fn modseq(&self) -> MessageDataItem<'static> { + MessageDataItem::ModSeq(self.in_idx.modseq) + } +} + +pub enum SeenFlag { + DoNothing, + MustAdd, +} + +// ------------------- + +pub enum FetchedMail<'a> { + IndexOnly, + Partial(AnyPart<'a>), + Full(AnyPart<'a>), +} +impl<'a> FetchedMail<'a> { + pub fn full_from_message(msg: Message<'a>) -> Self { + Self::Full(AnyPart::Msg(msg)) + } + + pub fn partial_from_message(msg: Message<'a>) -> Self { + Self::Partial(AnyPart::Msg(msg)) + } + + pub fn as_anypart(&self) -> Result<&AnyPart<'a>> { + match self { + FetchedMail::Full(x) => Ok(&x), + FetchedMail::Partial(x) => Ok(&x), + _ => bail!("The full message must be fetched, not only its headers"), + } + } + + pub fn as_msg(&self) -> Result<&Message<'a>> { + match self { + FetchedMail::Full(AnyPart::Msg(x)) => Ok(&x), + FetchedMail::Partial(AnyPart::Msg(x)) => Ok(&x), + _ => bail!("The full message must be fetched, not only its headers AND it must be an AnyPart::Msg."), + } + } + + pub fn as_imf(&self) -> Option<&imf::Imf<'a>> { + match self { + FetchedMail::Full(AnyPart::Msg(x)) => Some(&x.imf), + FetchedMail::Partial(AnyPart::Msg(x)) => Some(&x.imf), + _ => None, + } + } +} diff --git a/aero-proto/src/imap/mailbox_view.rs b/aero-proto/src/imap/mailbox_view.rs new file mode 100644 index 0000000..5154359 --- /dev/null +++ b/aero-proto/src/imap/mailbox_view.rs @@ -0,0 +1,772 @@ +use std::collections::HashSet; +use std::num::{NonZeroU32, NonZeroU64}; +use std::sync::Arc; + +use anyhow::{anyhow, Error, Result}; + +use futures::stream::{StreamExt, TryStreamExt}; + +use imap_codec::imap_types::core::Charset; +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}; +use imap_codec::imap_types::search::SearchKey; +use imap_codec::imap_types::sequence::SequenceSet; + +use aero_collections::mail::mailbox::Mailbox; +use aero_collections::mail::query::QueryScope; +use aero_collections::mail::snapshot::FrozenMailbox; +use aero_collections::mail::uidindex::{ImapUid, ImapUidvalidity, ModSeq}; +use aero_collections::mail::unique_ident::UniqueIdent; + +use crate::imap::attributes::AttributesProxy; +use crate::imap::flags; +use crate::imap::index::Index; +use crate::imap::mail_view::{MailView, SeenFlag}; +use crate::imap::response::Body; +use crate::imap::search; + +const DEFAULT_FLAGS: [Flag; 5] = [ + Flag::Seen, + Flag::Answered, + Flag::Flagged, + Flag::Deleted, + Flag::Draft, +]; + +pub struct UpdateParameters { + pub silence: HashSet, + pub with_modseq: bool, + pub with_uid: bool, +} +impl Default for UpdateParameters { + fn default() -> Self { + Self { + silence: HashSet::new(), + with_modseq: false, + with_uid: false, + } + } +} + +/// A MailboxView is responsible for giving the client the information +/// it needs about a mailbox, such as an initial summary of the mailbox's +/// content and continuous updates indicating when the content +/// of the mailbox has been changed. +/// To do this, it keeps a variable `known_state` that corresponds to +/// what the client knows, and produces IMAP messages to be sent to the +/// client that go along updates to `known_state`. +pub struct MailboxView { + pub internal: FrozenMailbox, + pub is_condstore: bool, +} + +impl MailboxView { + /// Creates a new IMAP view into a mailbox. + pub async fn new(mailbox: Arc, is_cond: bool) -> Self { + Self { + internal: mailbox.frozen().await, + is_condstore: is_cond, + } + } + + /// Create an updated view, useful to make a diff + /// between what the client knows and new stuff + /// Produces a set of IMAP responses describing the change between + /// what the client knows and what is actually in the mailbox. + /// This does NOT trigger a sync, it bases itself on what is currently + /// loaded in RAM by Bayou. + pub async fn update(&mut self, params: UpdateParameters) -> Result>> { + let old_snapshot = self.internal.update().await; + let new_snapshot = &self.internal.snapshot; + + let mut data = Vec::::new(); + + // Calculate diff between two mailbox states + // See example in IMAP RFC in section on NOOP command: + // we want to produce something like this: + // C: a047 NOOP + // S: * 22 EXPUNGE + // S: * 23 EXISTS + // S: * 14 FETCH (UID 1305 FLAGS (\Seen \Deleted)) + // S: a047 OK Noop completed + // In other words: + // - notify client of expunged mails + // - if new mails arrived, notify client of number of existing mails + // - if flags changed for existing mails, tell client + // (for this last step: if uidvalidity changed, do nothing, + // just notify of new uidvalidity and they will resync) + + // - notify client of expunged mails + let mut n_expunge = 0; + for (i, (_uid, uuid)) in old_snapshot.idx_by_uid.iter().enumerate() { + if !new_snapshot.table.contains_key(uuid) { + data.push(Body::Data(Data::Expunge( + NonZeroU32::try_from((i + 1 - n_expunge) as u32).unwrap(), + ))); + n_expunge += 1; + } + } + + // - if new mails arrived, notify client of number of existing mails + if new_snapshot.table.len() != old_snapshot.table.len() - n_expunge + || new_snapshot.uidvalidity != old_snapshot.uidvalidity + { + data.push(self.exists_status()?); + } + + if new_snapshot.uidvalidity != old_snapshot.uidvalidity { + // TODO: do we want to push less/more info than this? + data.push(self.uidvalidity_status()?); + data.push(self.uidnext_status()?); + } else { + // - if flags changed for existing mails, tell client + for (i, (_uid, uuid)) in new_snapshot.idx_by_uid.iter().enumerate() { + if params.silence.contains(uuid) { + continue; + } + + let old_mail = old_snapshot.table.get(uuid); + let new_mail = new_snapshot.table.get(uuid); + if old_mail.is_some() && old_mail != new_mail { + if let Some((uid, modseq, flags)) = new_mail { + let mut items = vec![MessageDataItem::Flags( + flags.iter().filter_map(|f| flags::from_str(f)).collect(), + )]; + + if params.with_uid { + items.push(MessageDataItem::Uid(*uid)); + } + + if params.with_modseq { + items.push(MessageDataItem::ModSeq(*modseq)); + } + + data.push(Body::Data(Data::Fetch { + seq: NonZeroU32::try_from((i + 1) as u32).unwrap(), + items: items.try_into()?, + })); + } + } + } + } + Ok(data) + } + + /// Generates the necessary IMAP messages so that the client + /// has a satisfactory summary of the current mailbox's state. + /// These are the messages that are sent in response to a SELECT command. + pub fn summary(&self) -> Result>> { + let mut data = Vec::::new(); + data.push(self.exists_status()?); + data.push(self.recent_status()?); + data.extend(self.flags_status()?.into_iter()); + data.push(self.uidvalidity_status()?); + data.push(self.uidnext_status()?); + if self.is_condstore { + data.push(self.highestmodseq_status()?); + } + /*self.unseen_first_status()? + .map(|unseen_status| data.push(unseen_status));*/ + + Ok(data) + } + + pub async fn store<'a>( + &mut self, + sequence_set: &SequenceSet, + kind: &StoreType, + response: &StoreResponse, + flags: &[Flag<'a>], + unchanged_since: Option, + is_uid_store: &bool, + ) -> Result<(Vec>, Vec)> { + self.internal.sync().await?; + + let flags = flags.iter().map(|x| x.to_string()).collect::>(); + + let idx = self.index()?; + let (editable, in_conflict) = + idx.fetch_unchanged_since(sequence_set, unchanged_since, *is_uid_store)?; + + for mi in editable.iter() { + match kind { + StoreType::Add => { + self.internal.mailbox.add_flags(mi.uuid, &flags[..]).await?; + } + StoreType::Remove => { + self.internal.mailbox.del_flags(mi.uuid, &flags[..]).await?; + } + StoreType::Replace => { + self.internal.mailbox.set_flags(mi.uuid, &flags[..]).await?; + } + } + } + + let silence = match response { + StoreResponse::Answer => HashSet::new(), + StoreResponse::Silent => editable.iter().map(|midx| midx.uuid).collect(), + }; + + let conflict_id_or_uid = match is_uid_store { + true => in_conflict.into_iter().map(|midx| midx.uid).collect(), + _ => in_conflict.into_iter().map(|midx| midx.i).collect(), + }; + + let summary = self + .update(UpdateParameters { + with_uid: *is_uid_store, + with_modseq: unchanged_since.is_some(), + silence, + }) + .await?; + + Ok((summary, conflict_id_or_uid)) + } + + pub async fn idle_sync(&mut self) -> Result>> { + self.internal + .mailbox + .notify() + .await + .upgrade() + .ok_or(anyhow!("test"))? + .notified() + .await; + self.internal.mailbox.opportunistic_sync().await?; + self.update(UpdateParameters::default()).await + } + + pub async fn expunge( + &mut self, + maybe_seq_set: &Option, + ) -> Result>> { + // Get a recent view to apply our change + self.internal.sync().await?; + let state = self.internal.peek().await; + let idx = Index::new(&state)?; + + // Build a default sequence set for the default case + use imap_codec::imap_types::sequence::{SeqOrUid, Sequence}; + let seq = match maybe_seq_set { + Some(s) => s.clone(), + None => SequenceSet( + vec![Sequence::Range( + SeqOrUid::Value(NonZeroU32::MIN), + SeqOrUid::Asterisk, + )] + .try_into() + .unwrap(), + ), + }; + + let deleted_flag = Flag::Deleted.to_string(); + let msgs = idx + .fetch_on_uid(&seq) + .into_iter() + .filter(|midx| midx.flags.iter().any(|x| *x == deleted_flag)) + .map(|midx| midx.uuid); + + for msg in msgs { + self.internal.mailbox.delete(msg).await?; + } + + self.update(UpdateParameters::default()).await + } + + pub async fn copy( + &self, + sequence_set: &SequenceSet, + to: Arc, + is_uid_copy: &bool, + ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>)> { + let idx = self.index()?; + let mails = idx.fetch(sequence_set, *is_uid_copy)?; + + let mut new_uuids = vec![]; + for mi in mails.iter() { + new_uuids.push(to.copy_from(&self.internal.mailbox, mi.uuid).await?); + } + + let mut ret = vec![]; + let to_state = to.current_uid_index().await; + for (mi, new_uuid) in mails.iter().zip(new_uuids.iter()) { + let dest_uid = to_state + .table + .get(new_uuid) + .ok_or(anyhow!("copied mail not in destination mailbox"))? + .0; + ret.push((mi.uid, dest_uid)); + } + + Ok((to_state.uidvalidity, ret)) + } + + pub async fn r#move( + &mut self, + sequence_set: &SequenceSet, + to: Arc, + is_uid_copy: &bool, + ) -> Result<(ImapUidvalidity, Vec<(ImapUid, ImapUid)>, Vec>)> { + let idx = self.index()?; + let mails = idx.fetch(sequence_set, *is_uid_copy)?; + + for mi in mails.iter() { + to.move_from(&self.internal.mailbox, mi.uuid).await?; + } + + let mut ret = vec![]; + let to_state = to.current_uid_index().await; + for mi in mails.iter() { + let dest_uid = to_state + .table + .get(&mi.uuid) + .ok_or(anyhow!("moved mail not in destination mailbox"))? + .0; + ret.push((mi.uid, dest_uid)); + } + + let update = self + .update(UpdateParameters { + with_uid: *is_uid_copy, + ..UpdateParameters::default() + }) + .await?; + + Ok((to_state.uidvalidity, ret, update)) + } + + /// Looks up state changes in the mailbox and produces a set of IMAP + /// responses describing the new state. + pub async fn fetch<'b>( + &self, + sequence_set: &SequenceSet, + ap: &AttributesProxy, + changed_since: Option, + is_uid_fetch: &bool, + ) -> Result>> { + // [1/6] Pre-compute data + // a. what are the uuids of the emails we want? + // b. do we need to fetch the full body? + //let ap = AttributesProxy::new(attributes, *is_uid_fetch); + let query_scope = match ap.need_body() { + true => QueryScope::Full, + _ => QueryScope::Partial, + }; + tracing::debug!("Query scope {:?}", query_scope); + let idx = self.index()?; + let mail_idx_list = idx.fetch_changed_since(sequence_set, changed_since, *is_uid_fetch)?; + + // [2/6] Fetch the emails + let uuids = mail_idx_list + .iter() + .map(|midx| midx.uuid) + .collect::>(); + + let query = self.internal.query(&uuids, query_scope); + //let query_result = self.internal.query(&uuids, query_scope).fetch().await?; + + 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), + }) + // [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. + query_stream.try_collect().await + } + + /// A naive search implementation... + pub async fn search<'a>( + &self, + _charset: &Option>, + search_key: &SearchKey<'a>, + uid: bool, + ) -> Result<(Vec>, bool)> { + // 1. Compute the subset of sequence identifiers we need to fetch + // based on the search query + let crit = search::Criteria(search_key); + let (seq_set, seq_type) = crit.to_sequence_set(); + + // 2. Get the selection + let idx = self.index()?; + let selection = idx.fetch(&seq_set, seq_type.is_uid())?; + + // 3. Filter the selection based on the ID / UID / Flags + let (kept_idx, to_fetch) = crit.filter_on_idx(&selection); + + // 4.a Fetch additional info about the emails + let query_scope = crit.query_scope(); + let uuids = to_fetch.iter().map(|midx| midx.uuid).collect::>(); + let query = self.internal.query(&uuids, query_scope); + + // 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) + }); + + // 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), + }); + + // 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, maybe_modseq))], + maybe_modseq.is_some(), + )) + } + + // ---- + /// @FIXME index should be stored for longer than a single request + /// Instead they should be tied to the FrozenMailbox refresh + /// It's not trivial to refactor the code to do that, so we are doing + /// some useless computation for now... + fn index<'a>(&'a self) -> Result> { + Index::new(&self.internal.snapshot) + } + + /// Produce an OK [UIDVALIDITY _] message corresponding to `known_state` + fn uidvalidity_status(&self) -> Result> { + let uid_validity = Status::ok( + None, + Some(Code::UidValidity(self.uidvalidity())), + "UIDs valid", + ) + .map_err(Error::msg)?; + Ok(Body::Status(uid_validity)) + } + + pub(crate) fn uidvalidity(&self) -> ImapUidvalidity { + self.internal.snapshot.uidvalidity + } + + /// Produce an OK [UIDNEXT _] message corresponding to `known_state` + fn uidnext_status(&self) -> Result> { + let next_uid = Status::ok( + None, + Some(Code::UidNext(self.uidnext())), + "Predict next UID", + ) + .map_err(Error::msg)?; + Ok(Body::Status(next_uid)) + } + + pub(crate) fn uidnext(&self) -> ImapUid { + self.internal.snapshot.uidnext + } + + pub(crate) fn highestmodseq_status(&self) -> Result> { + Ok(Body::Status(Status::ok( + None, + Some(Code::Other(CodeOther::unvalidated( + format!("HIGHESTMODSEQ {}", self.highestmodseq()).into_bytes(), + ))), + "Highest", + )?)) + } + + pub(crate) fn highestmodseq(&self) -> ModSeq { + self.internal.snapshot.highestmodseq + } + + /// Produce an EXISTS message corresponding to the number of mails + /// in `known_state` + fn exists_status(&self) -> Result> { + Ok(Body::Data(Data::Exists(self.exists()?))) + } + + pub(crate) fn exists(&self) -> Result { + Ok(u32::try_from(self.internal.snapshot.idx_by_uid.len())?) + } + + /// Produce a RECENT message corresponding to the number of + /// recent mails in `known_state` + fn recent_status(&self) -> Result> { + Ok(Body::Data(Data::Recent(self.recent()?))) + } + + #[allow(dead_code)] + fn unseen_first_status(&self) -> Result>> { + Ok(self + .unseen_first()? + .map(|unseen_id| { + Status::ok(None, Some(Code::Unseen(unseen_id)), "First unseen.").map(Body::Status) + }) + .transpose()?) + } + + #[allow(dead_code)] + fn unseen_first(&self) -> Result> { + Ok(self + .internal + .snapshot + .table + .values() + .enumerate() + .find(|(_i, (_imap_uid, _modseq, flags))| !flags.contains(&"\\Seen".to_string())) + .map(|(i, _)| NonZeroU32::try_from(i as u32 + 1)) + .transpose()?) + } + + pub(crate) fn recent(&self) -> Result { + let recent = self + .internal + .snapshot + .idx_by_flag + .get(&"\\Recent".to_string()) + .map(|os| os.len()) + .unwrap_or(0); + Ok(u32::try_from(recent)?) + } + + /// Produce a FLAGS and a PERMANENTFLAGS message that indicates + /// the flags that are in `known_state` + default flags + fn flags_status(&self) -> Result>> { + let mut body = vec![]; + + // 1. Collecting all the possible flags in the mailbox + // 1.a Fetch them from our index + let mut known_flags: Vec = self + .internal + .snapshot + .idx_by_flag + .flags() + .filter_map(|f| match flags::from_str(f) { + Some(FlagFetch::Flag(fl)) => Some(fl), + _ => None, + }) + .collect(); + // 1.b Merge it with our default flags list + for f in DEFAULT_FLAGS.iter() { + if !known_flags.contains(f) { + known_flags.push(f.clone()); + } + } + // 1.c Create the IMAP message + body.push(Body::Data(Data::Flags(known_flags.clone()))); + + // 2. Returning flags that are persisted + // 2.a Always advertise our default flags + let mut permanent = DEFAULT_FLAGS + .iter() + .map(|f| FlagPerm::Flag(f.clone())) + .collect::>(); + // 2.b Say that we support any keyword flag + permanent.push(FlagPerm::Asterisk); + // 2.c Create the IMAP message + let permanent_flags = Status::ok( + None, + Some(Code::PermanentFlags(permanent)), + "Flags permitted", + ) + .map_err(Error::msg)?; + body.push(Body::Status(permanent_flags)); + + // Done! + Ok(body) + } + + pub(crate) fn unseen_count(&self) -> usize { + let total = self.internal.snapshot.table.len(); + let seen = self + .internal + .snapshot + .idx_by_flag + .get(&Flag::Seen.to_string()) + .map(|x| x.len()) + .unwrap_or(0); + total - seen + } +} + +#[cfg(test)] +mod tests { + use super::*; + use imap_codec::encode::Encoder; + 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; + use imap_codec::ResponseCodec; + use std::fs; + + use crate::cryptoblob; + use crate::imap::index::MailIndex; + use crate::imap::mail_view::MailView; + use crate::imap::mime_view; + use crate::mail::mailbox::MailMeta; + use crate::mail::query::QueryResult; + use crate::mail::unique_ident; + + #[test] + fn mailview_body_ext() -> Result<()> { + let ap = AttributesProxy::new( + &MacroOrMessageDataItemNames::MessageDataItemNames(vec![ + MessageDataItemName::BodyExt { + section: Some(Section::Header(None)), + partial: None, + peek: false, + }, + ]), + &[], + false, + ); + + let key = cryptoblob::gen_key(); + let meta = MailMeta { + internaldate: 0u64, + headers: vec![], + message_key: key, + rfc822_size: 8usize, + }; + + let index_entry = (NonZeroU32::MIN, NonZeroU64::MIN, vec![]); + let mail_in_idx = MailIndex { + i: NonZeroU32::MIN, + uid: index_entry.0, + modseq: index_entry.1, + uuid: unique_ident::gen_ident(), + flags: &index_entry.2, + }; + let rfc822 = b"Subject: hello\r\nFrom: a@a.a\r\nTo: b@b.b\r\nDate: Thu, 12 Oct 2023 08:45:28 +0000\r\n\r\nhello world"; + let qr = QueryResult::FullResult { + uuid: mail_in_idx.uuid.clone(), + metadata: meta, + content: rfc822.to_vec(), + }; + + let mv = MailView::new(&qr, &mail_in_idx)?; + let (res_body, _seen) = mv.filter(&ap)?; + + let fattr = match res_body { + Body::Data(Data::Fetch { + seq: _seq, + items: attr, + }) => Ok(attr), + _ => Err(anyhow!("Not a fetch body")), + }?; + + assert_eq!(fattr.as_ref().len(), 1); + + let (sec, _orig, _data) = match &fattr.as_ref()[0] { + MessageDataItem::BodyExt { + section, + origin, + data, + } => Ok((section, origin, data)), + _ => Err(anyhow!("not a body ext message attribute")), + }?; + + assert_eq!(sec.as_ref().unwrap(), &Section::Header(None)); + + Ok(()) + } + + /// Future automated test. We use lossy utf8 conversion + lowercase everything, + /// so this test might allow invalid results. But at least it allows us to quickly test a + /// large variety of emails. + /// Keep in mind that special cases must still be tested manually! + #[test] + fn fetch_body() -> Result<()> { + let prefixes = [ + /* *** MY OWN DATASET *** */ + "tests/emails/dxflrs/0001_simple", + "tests/emails/dxflrs/0002_mime", + "tests/emails/dxflrs/0003_mime-in-mime", + "tests/emails/dxflrs/0004_msg-in-msg", + // eml_codec do not support continuation for the moment + //"tests/emails/dxflrs/0005_mail-parser-readme", + "tests/emails/dxflrs/0006_single-mime", + "tests/emails/dxflrs/0007_raw_msg_in_rfc822", + /* *** (STRANGE) RFC *** */ + //"tests/emails/rfc/000", // must return text/enriched, we return text/plain + //"tests/emails/rfc/001", // does not recognize the multipart/external-body, breaks the + // whole parsing + //"tests/emails/rfc/002", // wrong date in email + + //"tests/emails/rfc/003", // dovecot fixes \r\r: the bytes number is wrong + text/enriched + + /* *** THIRD PARTY *** */ + //"tests/emails/thirdparty/000", // dovecot fixes \r\r: the bytes number is wrong + //"tests/emails/thirdparty/001", // same + "tests/emails/thirdparty/002", // same + + /* *** LEGACY *** */ + //"tests/emails/legacy/000", // same issue with \r\r + ]; + + for pref in prefixes.iter() { + println!("{}", pref); + let txt = fs::read(format!("{}.eml", pref))?; + let oracle = fs::read(format!("{}.dovecot.body", pref))?; + let message = eml_codec::parse_message(&txt).unwrap().1; + + let test_repr = Response::Data(Data::Fetch { + seq: NonZeroU32::new(1).unwrap(), + items: Vec1::from(MessageDataItem::Body(mime_view::bodystructure( + &message.child, + false, + )?)), + }); + let test_bytes = ResponseCodec::new().encode(&test_repr).dump(); + let test_str = String::from_utf8_lossy(&test_bytes).to_lowercase(); + + let oracle_str = + format!("* 1 FETCH {}\r\n", String::from_utf8_lossy(&oracle)).to_lowercase(); + + println!("aerogramme: {}\n\ndovecot: {}\n\n", test_str, oracle_str); + //println!("\n\n {} \n\n", String::from_utf8_lossy(&resp)); + assert_eq!(test_str, oracle_str); + } + + Ok(()) + } +} diff --git a/aero-proto/src/imap/mime_view.rs b/aero-proto/src/imap/mime_view.rs new file mode 100644 index 0000000..720f20a --- /dev/null +++ b/aero-proto/src/imap/mime_view.rs @@ -0,0 +1,582 @@ +use std::borrow::Cow; +use std::collections::HashSet; +use std::num::NonZeroU32; + +use anyhow::{anyhow, bail, Result}; + +use imap_codec::imap_types::body::{ + BasicFields, Body as FetchBody, BodyStructure, MultiPartExtensionData, SinglePartExtensionData, + SpecificFields, +}; +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::{ + header, mime, mime::r#type::Deductible, part::composite, part::discrete, part::AnyPart, +}; + +use crate::imap::imf_view::ImfView; + +pub enum BodySection<'a> { + Full(Cow<'a, [u8]>), + Slice { + body: Cow<'a, [u8]>, + origin_octet: u32, + }, +} + +/// Logic for BODY[
]<> +/// Works in 3 times: +/// 1. Find the section (RootMime::subset) +/// 2. Apply the extraction logic (SelectedMime::extract), like TEXT, HEADERS, etc. +/// 3. Keep only the given subset provided by partial +/// +/// Example of message sections: +/// +/// ``` +/// HEADER ([RFC-2822] header of the message) +/// TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED +/// 1 TEXT/PLAIN +/// 2 APPLICATION/OCTET-STREAM +/// 3 MESSAGE/RFC822 +/// 3.HEADER ([RFC-2822] header of the message) +/// 3.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED +/// 3.1 TEXT/PLAIN +/// 3.2 APPLICATION/OCTET-STREAM +/// 4 MULTIPART/MIXED +/// 4.1 IMAGE/GIF +/// 4.1.MIME ([MIME-IMB] header for the IMAGE/GIF) +/// 4.2 MESSAGE/RFC822 +/// 4.2.HEADER ([RFC-2822] header of the message) +/// 4.2.TEXT ([RFC-2822] text body of the message) MULTIPART/MIXED +/// 4.2.1 TEXT/PLAIN +/// 4.2.2 MULTIPART/ALTERNATIVE +/// 4.2.2.1 TEXT/PLAIN +/// 4.2.2.2 TEXT/RICHTEXT +/// ``` +pub fn body_ext<'a>( + part: &'a AnyPart<'a>, + section: &'a Option>, + partial: &'a Option<(u32, NonZeroU32)>, +) -> Result> { + let root_mime = NodeMime(part); + let (extractor, path) = SubsettedSection::from(section); + let selected_mime = root_mime.subset(path)?; + let extracted_full = selected_mime.extract(&extractor)?; + Ok(extracted_full.to_body_section(partial)) +} + +/// Logic for BODY and BODYSTRUCTURE +/// +/// ```raw +/// b fetch 29878:29879 (BODY) +/// * 29878 FETCH (BODY (("text" "plain" ("charset" "utf-8") NIL NIL "quoted-printable" 3264 82)("text" "html" ("charset" "utf-8") NIL NIL "quoted-printable" 31834 643) "alternative")) +/// * 29879 FETCH (BODY ("text" "html" ("charset" "us-ascii") NIL NIL "7bit" 4107 131)) +/// ^^^^^^^^^^^^^^^^^^^^^^ ^^^ ^^^ ^^^^^^ ^^^^ ^^^ +/// | | | | | | number of lines +/// | | | | | size +/// | | | | content transfer encoding +/// | | | description +/// | | id +/// | parameter list +/// b OK Fetch completed (0.001 + 0.000 secs). +/// ``` +pub fn bodystructure(part: &AnyPart, is_ext: bool) -> Result> { + NodeMime(part).structure(is_ext) +} + +/// NodeMime +/// +/// Used for recursive logic on MIME. +/// See SelectedMime for inspection. +struct NodeMime<'a>(&'a AnyPart<'a>); +impl<'a> NodeMime<'a> { + /// A MIME object is a tree of elements. + /// The path indicates which element must be picked. + /// This function returns the picked element as the new view + fn subset(self, path: Option<&'a FetchPart>) -> Result> { + match path { + None => Ok(SelectedMime(self.0)), + Some(v) => self.rec_subset(v.0.as_ref()), + } + } + + fn rec_subset(self, path: &'a [NonZeroU32]) -> Result { + if path.is_empty() { + Ok(SelectedMime(self.0)) + } else { + match self.0 { + AnyPart::Mult(x) => { + let next = Self(x.children + .get(path[0].get() as usize - 1) + .ok_or(anyhow!("Unable to resolve subpath {:?}, current multipart has only {} elements", path, x.children.len()))?); + next.rec_subset(&path[1..]) + }, + AnyPart::Msg(x) => { + let next = Self(x.child.as_ref()); + next.rec_subset(path) + }, + _ => bail!("You tried to access a subpart on an atomic part (text or binary). Unresolved subpath {:?}", path), + } + } + } + + fn structure(&self, is_ext: bool) -> Result> { + match self.0 { + AnyPart::Txt(x) => NodeTxt(self, x).structure(is_ext), + AnyPart::Bin(x) => NodeBin(self, x).structure(is_ext), + AnyPart::Mult(x) => NodeMult(self, x).structure(is_ext), + AnyPart::Msg(x) => NodeMsg(self, x).structure(is_ext), + } + } +} + +//---------------------------------------------------------- + +/// A FetchSection must be handled in 2 times: +/// - First we must extract the MIME part +/// - Then we must process it as desired +/// The given struct mixes both work, so +/// we separate this work here. +enum SubsettedSection<'a> { + Part, + Header, + HeaderFields(&'a Vec1>), + HeaderFieldsNot(&'a Vec1>), + Text, + Mime, +} +impl<'a> SubsettedSection<'a> { + fn from(section: &'a Option) -> (Self, Option<&'a FetchPart>) { + match section { + Some(FetchSection::Text(maybe_part)) => (Self::Text, maybe_part.as_ref()), + Some(FetchSection::Header(maybe_part)) => (Self::Header, maybe_part.as_ref()), + Some(FetchSection::HeaderFields(maybe_part, fields)) => { + (Self::HeaderFields(fields), maybe_part.as_ref()) + } + Some(FetchSection::HeaderFieldsNot(maybe_part, fields)) => { + (Self::HeaderFieldsNot(fields), maybe_part.as_ref()) + } + Some(FetchSection::Mime(part)) => (Self::Mime, Some(part)), + Some(FetchSection::Part(part)) => (Self::Part, Some(part)), + None => (Self::Part, None), + } + } +} + +/// Used for current MIME inspection +/// +/// See NodeMime for recursive logic +pub struct SelectedMime<'a>(pub &'a AnyPart<'a>); +impl<'a> SelectedMime<'a> { + pub fn header_value(&'a self, to_match_ext: &[u8]) -> Option<&'a [u8]> { + let to_match = to_match_ext.to_ascii_lowercase(); + + self.eml_mime() + .kv + .iter() + .filter_map(|field| match field { + header::Field::Good(header::Kv2(k, v)) => Some((k, v)), + _ => None, + }) + .find(|(k, _)| k.to_ascii_lowercase() == to_match) + .map(|(_, v)| v) + .copied() + } + + /// The subsetted fetch section basically tells us the + /// extraction logic to apply on our selected MIME. + /// This function acts as a router for these logic. + fn extract(&self, extractor: &SubsettedSection<'a>) -> Result> { + match extractor { + SubsettedSection::Text => self.text(), + SubsettedSection::Header => self.header(), + SubsettedSection::HeaderFields(fields) => self.header_fields(fields, false), + SubsettedSection::HeaderFieldsNot(fields) => self.header_fields(fields, true), + SubsettedSection::Part => self.part(), + SubsettedSection::Mime => self.mime(), + } + } + + fn mime(&self) -> Result> { + let bytes = match &self.0 { + AnyPart::Txt(p) => p.mime.fields.raw, + AnyPart::Bin(p) => p.mime.fields.raw, + AnyPart::Msg(p) => p.child.mime().raw, + AnyPart::Mult(p) => p.mime.fields.raw, + }; + Ok(ExtractedFull(bytes.into())) + } + + fn part(&self) -> Result> { + let bytes = match &self.0 { + AnyPart::Txt(p) => p.body, + AnyPart::Bin(p) => p.body, + AnyPart::Msg(p) => p.raw_part, + AnyPart::Mult(_) => bail!("Multipart part has no body"), + }; + Ok(ExtractedFull(bytes.to_vec().into())) + } + + fn eml_mime(&self) -> &eml_codec::mime::NaiveMIME<'_> { + match &self.0 { + AnyPart::Msg(msg) => msg.child.mime(), + other => other.mime(), + } + } + + /// The [...] HEADER.FIELDS, and HEADER.FIELDS.NOT part + /// specifiers refer to the [RFC-2822] header of the message or of + /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. + /// HEADER.FIELDS and HEADER.FIELDS.NOT are followed by a list of + /// field-name (as defined in [RFC-2822]) names, and return a + /// subset of the header. The subset returned by HEADER.FIELDS + /// contains only those header fields with a field-name that + /// matches one of the names in the list; similarly, the subset + /// returned by HEADER.FIELDS.NOT contains only the header fields + /// with a non-matching field-name. The field-matching is + /// case-insensitive but otherwise exact. + fn header_fields( + &self, + fields: &'a Vec1>, + invert: bool, + ) -> Result> { + // Build a lowercase ascii hashset with the fields to fetch + let index = fields + .as_ref() + .iter() + .map(|x| { + match x { + AString::Atom(a) => a.inner().as_bytes(), + AString::String(IString::Literal(l)) => l.as_ref(), + AString::String(IString::Quoted(q)) => q.inner().as_bytes(), + } + .to_ascii_lowercase() + }) + .collect::>(); + + // Extract MIME headers + let mime = self.eml_mime(); + + // Filter our MIME headers based on the field index + // 1. Keep only the correctly formatted headers + // 2. Keep only based on the index presence or absence + // 3. Reduce as a byte vector + let buffer = mime + .kv + .iter() + .filter_map(|field| match field { + header::Field::Good(header::Kv2(k, v)) => Some((k, v)), + _ => None, + }) + .filter(|(k, _)| index.contains(&k.to_ascii_lowercase()) ^ invert) + .fold(vec![], |mut acc, (k, v)| { + acc.extend(*k); + acc.extend(b": "); + acc.extend(*v); + acc.extend(b"\r\n"); + acc + }); + + Ok(ExtractedFull(buffer.into())) + } + + /// The HEADER [...] part specifiers refer to the [RFC-2822] header of the message or of + /// an encapsulated [MIME-IMT] MESSAGE/RFC822 message. + /// ```raw + /// HEADER ([RFC-2822] header of the message) + /// ``` + fn header(&self) -> Result> { + let msg = self + .0 + .as_message() + .ok_or(anyhow!("Selected part must be a message/rfc822"))?; + Ok(ExtractedFull(msg.raw_headers.into())) + } + + /// The TEXT part specifier refers to the text body of the message, omitting the [RFC-2822] header. + fn text(&self) -> Result> { + let msg = self + .0 + .as_message() + .ok_or(anyhow!("Selected part must be a message/rfc822"))?; + Ok(ExtractedFull(msg.raw_body.into())) + } + + // ------------ + + /// Basic field of a MIME part that is + /// common to all parts + fn basic_fields(&self) -> Result> { + let sz = match self.0 { + AnyPart::Txt(x) => x.body.len(), + AnyPart::Bin(x) => x.body.len(), + AnyPart::Msg(x) => x.raw_part.len(), + AnyPart::Mult(_) => 0, + }; + let m = self.0.mime(); + let parameter_list = m + .ctype + .as_ref() + .map(|x| { + x.params + .iter() + .map(|p| { + ( + IString::try_from(String::from_utf8_lossy(p.name).to_string()), + IString::try_from(p.value.to_string()), + ) + }) + .filter(|(k, v)| k.is_ok() && v.is_ok()) + .map(|(k, v)| (k.unwrap(), v.unwrap())) + .collect() + }) + .unwrap_or(vec![]); + + Ok(BasicFields { + parameter_list, + id: NString( + m.id.as_ref() + .and_then(|ci| IString::try_from(ci.to_string()).ok()), + ), + description: NString( + m.description + .as_ref() + .and_then(|cd| IString::try_from(cd.to_string()).ok()), + ), + content_transfer_encoding: match m.transfer_encoding { + mime::mechanism::Mechanism::_8Bit => unchecked_istring("8bit"), + mime::mechanism::Mechanism::Binary => unchecked_istring("binary"), + mime::mechanism::Mechanism::QuotedPrintable => { + unchecked_istring("quoted-printable") + } + mime::mechanism::Mechanism::Base64 => unchecked_istring("base64"), + _ => unchecked_istring("7bit"), + }, + // @FIXME we can't compute the size of the message currently... + size: u32::try_from(sz)?, + }) + } +} + +// --------------------------- +struct NodeMsg<'a>(&'a NodeMime<'a>, &'a composite::Message<'a>); +impl<'a> NodeMsg<'a> { + fn structure(&self, is_ext: bool) -> Result> { + let basic = SelectedMime(self.0 .0).basic_fields()?; + + Ok(BodyStructure::Single { + body: FetchBody { + basic, + specific: SpecificFields::Message { + envelope: Box::new(ImfView(&self.1.imf).message_envelope()), + body_structure: Box::new(NodeMime(&self.1.child).structure(is_ext)?), + number_of_lines: nol(self.1.raw_part), + }, + }, + extension_data: match is_ext { + true => Some(SinglePartExtensionData { + md5: NString(None), + tail: None, + }), + _ => None, + }, + }) + } +} + +#[allow(dead_code)] +struct NodeMult<'a>(&'a NodeMime<'a>, &'a composite::Multipart<'a>); +impl<'a> NodeMult<'a> { + fn structure(&self, is_ext: bool) -> Result> { + let itype = &self.1.mime.interpreted_type; + let subtype = IString::try_from(itype.subtype.to_string()) + .unwrap_or(unchecked_istring("alternative")); + + let inner_bodies = self + .1 + .children + .iter() + .filter_map(|inner| NodeMime(&inner).structure(is_ext).ok()) + .collect::>(); + + Vec1::validate(&inner_bodies)?; + let bodies = Vec1::unvalidated(inner_bodies); + + Ok(BodyStructure::Multi { + bodies, + subtype, + extension_data: match is_ext { + true => Some(MultiPartExtensionData { + parameter_list: vec![( + IString::try_from("boundary").unwrap(), + IString::try_from(self.1.mime.interpreted_type.boundary.to_string())?, + )], + tail: None, + }), + _ => None, + }, + }) + } +} +struct NodeTxt<'a>(&'a NodeMime<'a>, &'a discrete::Text<'a>); +impl<'a> NodeTxt<'a> { + fn structure(&self, is_ext: bool) -> Result> { + let mut basic = SelectedMime(self.0 .0).basic_fields()?; + + // Get the interpreted content type, set it + let itype = match &self.1.mime.interpreted_type { + Deductible::Inferred(v) | Deductible::Explicit(v) => v, + }; + let subtype = + IString::try_from(itype.subtype.to_string()).unwrap_or(unchecked_istring("plain")); + + // Add charset to the list of parameters if we know it has been inferred as it will be + // missing from the parsed content. + if let Deductible::Inferred(charset) = &itype.charset { + basic.parameter_list.push(( + unchecked_istring("charset"), + IString::try_from(charset.to_string()).unwrap_or(unchecked_istring("us-ascii")), + )); + } + + Ok(BodyStructure::Single { + body: FetchBody { + basic, + specific: SpecificFields::Text { + subtype, + number_of_lines: nol(self.1.body), + }, + }, + extension_data: match is_ext { + true => Some(SinglePartExtensionData { + md5: NString(None), + tail: None, + }), + _ => None, + }, + }) + } +} + +struct NodeBin<'a>(&'a NodeMime<'a>, &'a discrete::Binary<'a>); +impl<'a> NodeBin<'a> { + fn structure(&self, is_ext: bool) -> Result> { + let basic = SelectedMime(self.0 .0).basic_fields()?; + + let default = mime::r#type::NaiveType { + main: &b"application"[..], + sub: &b"octet-stream"[..], + params: vec![], + }; + let ct = self.1.mime.fields.ctype.as_ref().unwrap_or(&default); + + let r#type = IString::try_from(String::from_utf8_lossy(ct.main).to_string()).or(Err( + anyhow!("Unable to build IString from given Content-Type type given"), + ))?; + + let subtype = IString::try_from(String::from_utf8_lossy(ct.sub).to_string()).or(Err( + anyhow!("Unable to build IString from given Content-Type subtype given"), + ))?; + + Ok(BodyStructure::Single { + body: FetchBody { + basic, + specific: SpecificFields::Basic { r#type, subtype }, + }, + extension_data: match is_ext { + true => Some(SinglePartExtensionData { + md5: NString(None), + tail: None, + }), + _ => None, + }, + }) + } +} + +// --------------------------- + +struct ExtractedFull<'a>(Cow<'a, [u8]>); +impl<'a> ExtractedFull<'a> { + /// It is possible to fetch a substring of the designated text. + /// This is done by appending an open angle bracket ("<"), the + /// octet position of the first desired octet, a period, the + /// maximum number of octets desired, and a close angle bracket + /// (">") to the part specifier. If the starting octet is beyond + /// the end of the text, an empty string is returned. + /// + /// Any partial fetch that attempts to read beyond the end of the + /// text is truncated as appropriate. A partial fetch that starts + /// at octet 0 is returned as a partial fetch, even if this + /// truncation happened. + /// + /// Note: This means that BODY[]<0.2048> of a 1500-octet message + /// will return BODY[]<0> with a literal of size 1500, not + /// BODY[]. + /// + /// Note: A substring fetch of a HEADER.FIELDS or + /// HEADER.FIELDS.NOT part specifier is calculated after + /// subsetting the header. + fn to_body_section(self, partial: &'_ Option<(u32, NonZeroU32)>) -> BodySection<'a> { + match partial { + Some((begin, len)) => self.partialize(*begin, *len), + None => BodySection::Full(self.0), + } + } + + fn partialize(self, begin: u32, len: NonZeroU32) -> BodySection<'a> { + // Asked range is starting after the end of the content, + // returning an empty buffer + if begin as usize > self.0.len() { + return BodySection::Slice { + body: Cow::Borrowed(&[][..]), + origin_octet: begin, + }; + } + + // Asked range is ending after the end of the content, + // slice only the beginning of the buffer + if (begin + len.get()) as usize >= self.0.len() { + return BodySection::Slice { + body: match self.0 { + Cow::Borrowed(body) => Cow::Borrowed(&body[begin as usize..]), + Cow::Owned(body) => Cow::Owned(body[begin as usize..].to_vec()), + }, + origin_octet: begin, + }; + } + + // Range is included inside the considered content, + // this is the "happy case" + BodySection::Slice { + body: match self.0 { + Cow::Borrowed(body) => { + Cow::Borrowed(&body[begin as usize..(begin + len.get()) as usize]) + } + Cow::Owned(body) => { + Cow::Owned(body[begin as usize..(begin + len.get()) as usize].to_vec()) + } + }, + origin_octet: begin, + } + } +} + +/// ---- LEGACY + +/// s is set to static to ensure that only compile time values +/// checked by developpers are passed. +fn unchecked_istring(s: &'static str) -> IString { + IString::try_from(s).expect("this value is expected to be a valid imap-codec::IString") +} + +// Number Of Lines +fn nol(input: &[u8]) -> u32 { + input + .iter() + .filter(|x| **x == b'\n') + .count() + .try_into() + .unwrap_or(0) +} diff --git a/aero-proto/src/imap/mod.rs b/aero-proto/src/imap/mod.rs new file mode 100644 index 0000000..ae3b58f --- /dev/null +++ b/aero-proto/src/imap/mod.rs @@ -0,0 +1,417 @@ +mod attributes; +mod capability; +mod command; +mod flags; +mod flow; +mod imf_view; +mod index; +mod mail_view; +mod mailbox_view; +mod mime_view; +mod request; +mod response; +mod search; +mod session; + +use std::net::SocketAddr; + +use anyhow::{anyhow, bail, Result}; +use futures::stream::{FuturesUnordered, StreamExt}; +use tokio::net::TcpListener; +use tokio::sync::mpsc; +use tokio::sync::watch; +use imap_codec::imap_types::response::{Code, CommandContinuationRequest, Response, Status}; +use imap_codec::imap_types::{core::Text, response::Greeting}; +use imap_flow::server::{ServerFlow, ServerFlowEvent, ServerFlowOptions}; +use imap_flow::stream::AnyStream; +use rustls_pemfile::{certs, private_key}; +use tokio_rustls::TlsAcceptor; + +use aero_user::config::{ImapConfig, ImapUnsecureConfig}; +use aero_user::login::ArcLoginProvider; + +use crate::imap::capability::ServerCapability; +use crate::imap::request::Request; +use crate::imap::response::{Body, ResponseOrIdle}; +use crate::imap::session::Instance; + +/// Server is a thin wrapper to register our Services in BàL +pub struct Server { + bind_addr: SocketAddr, + login_provider: ArcLoginProvider, + capabilities: ServerCapability, + tls: Option, +} + +#[derive(Clone)] +struct ClientContext { + addr: SocketAddr, + login_provider: ArcLoginProvider, + must_exit: watch::Receiver, + server_capabilities: ServerCapability, +} + +pub fn new(config: ImapConfig, login: ArcLoginProvider) -> Result { + let loaded_certs = certs(&mut std::io::BufReader::new(std::fs::File::open( + config.certs, + )?)) + .collect::, _>>()?; + 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() + .with_single_cert(loaded_certs, loaded_key)?; + let acceptor = TlsAcceptor::from(Arc::new(tls_config)); + + Ok(Server { + bind_addr: config.bind_addr, + login_provider: login, + capabilities: ServerCapability::default(), + tls: Some(acceptor), + }) +} + +pub fn new_unsecure(config: ImapUnsecureConfig, login: ArcLoginProvider) -> Server { + Server { + bind_addr: config.bind_addr, + login_provider: login, + capabilities: ServerCapability::default(), + tls: None, + } +} + +impl Server { + pub async fn run(self: Self, mut must_exit: watch::Receiver) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + tracing::info!("IMAP server listening on {:#}", self.bind_addr); + + let mut connections = FuturesUnordered::new(); + + while !*must_exit.borrow() { + let wait_conn_finished = async { + if connections.is_empty() { + futures::future::pending().await + } else { + connections.next().await + } + }; + let (socket, remote_addr) = tokio::select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + tracing::info!("IMAP: accepted connection from {}", remote_addr); + let stream = match self.tls.clone() { + Some(acceptor) => { + let stream = match acceptor.accept(socket).await { + Ok(v) => v, + Err(e) => { + tracing::error!(err=?e, "TLS negociation failed"); + continue; + } + }; + AnyStream::new(stream) + } + None => AnyStream::new(socket), + }; + + let client = ClientContext { + addr: remote_addr.clone(), + login_provider: self.login_provider.clone(), + must_exit: must_exit.clone(), + server_capabilities: self.capabilities.clone(), + }; + let conn = tokio::spawn(NetLoop::handler(client, stream)); + connections.push(conn); + } + drop(tcp); + + tracing::info!("IMAP server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) + } +} + +use std::sync::Arc; +use tokio::sync::mpsc::*; +use tokio::sync::Notify; + +const PIPELINABLE_COMMANDS: usize = 64; + +// @FIXME a full refactor of this part of the code will be needed sooner or later +struct NetLoop { + ctx: ClientContext, + server: ServerFlow, + cmd_tx: Sender, + resp_rx: UnboundedReceiver, +} + +impl NetLoop { + async fn handler(ctx: ClientContext, sock: AnyStream) { + let addr = ctx.addr.clone(); + + let mut nl = match Self::new(ctx, sock).await { + Ok(nl) => { + tracing::debug!(addr=?addr, "netloop successfully initialized"); + nl + } + Err(e) => { + tracing::error!(addr=?addr, err=?e, "netloop can not be initialized, closing session"); + return; + } + }; + + match nl.core().await { + Ok(()) => { + tracing::debug!("closing successful netloop core for {:?}", addr); + } + Err(e) => { + tracing::error!("closing errored netloop core for {:?}: {}", addr, e); + } + } + } + + async fn new(ctx: ClientContext, sock: AnyStream) -> Result { + 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, + opts, + Greeting::ok( + Some(Code::Capability(ctx.server_capabilities.to_vec())), + "Aerogramme", + ) + .unwrap(), + ) + .await?; + + // Start a mailbox session in background + let (cmd_tx, cmd_rx) = mpsc::channel::(PIPELINABLE_COMMANDS); + let (resp_tx, resp_rx) = mpsc::unbounded_channel::(); + tokio::spawn(Self::session(ctx.clone(), cmd_rx, resp_tx)); + + // Return the object + Ok(NetLoop { + ctx, + server, + cmd_tx, + resp_rx, + }) + } + + /// Coms with the background session + async fn session( + ctx: ClientContext, + mut cmd_rx: Receiver, + resp_tx: UnboundedSender, + ) -> () { + let mut session = Instance::new(ctx.login_provider, ctx.server_capabilities); + loop { + let cmd = match cmd_rx.recv().await { + None => break, + Some(cmd_recv) => cmd_recv, + }; + + tracing::debug!(cmd=?cmd, sock=%ctx.addr, "command"); + let maybe_response = session.request(cmd).await; + tracing::debug!(cmd=?maybe_response, sock=%ctx.addr, "response"); + + match resp_tx.send(maybe_response) { + Err(_) => break, + Ok(_) => (), + }; + } + tracing::info!("runner is quitting"); + } + + async fn core(&mut self) -> Result<()> { + let mut maybe_idle: Option> = None; + loop { + 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); + }, + }, + + // 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()); + }, + }; + } + } + + /* + async fn idle_mode(&mut self, mut buff: BytesMut, stop: Arc) -> Result { + // Flush send + loop { + tracing::trace!("flush server send"); + match self.server.progress_send().await? { + Some(..) => continue, + None => break, + } + } + + tokio::select! { + // Receiving IDLE event from background + 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), + Body::Status(s) => self.server.enqueue_status(s), + }; + } + self.server.enqueue_status(response.completion); + return Ok(LoopMode::Interactive) + }, + // 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), + Body::Status(s) => self.server.enqueue_status(s), + }; + } + self.cmd_tx.try_send(Request::Idle)?; + return Ok(LoopMode::Idle(buff, stop)) + }, + + // Session crashed + None => { + self.server.enqueue_status(Status::bye(None, "Internal session exited").unwrap()); + tracing::error!("session task exited for {:?}, quitting", self.ctx.addr); + return Ok(LoopMode::Interactive) + }, + + // Session can't start idling while already idling, it's a logic error! + Some(ResponseOrIdle::StartIdle(..)) => bail!("can't start idling while already idling!"), + }, + + // User is trying to interact with us + 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(_) => { + 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."), + }; + + return Ok(LoopMode::Idle(buff, stop)) + }, + + // 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) + }, + }; + }*/ +} diff --git a/aero-proto/src/imap/request.rs b/aero-proto/src/imap/request.rs new file mode 100644 index 0000000..cff18a3 --- /dev/null +++ b/aero-proto/src/imap/request.rs @@ -0,0 +1,9 @@ +use imap_codec::imap_types::command::Command; +use imap_codec::imap_types::core::Tag; + +#[derive(Debug)] +pub enum Request { + ImapCommand(Command<'static>), + IdleStart(Tag<'static>), + IdlePoll, +} diff --git a/aero-proto/src/imap/response.rs b/aero-proto/src/imap/response.rs new file mode 100644 index 0000000..b6a0e98 --- /dev/null +++ b/aero-proto/src/imap/response.rs @@ -0,0 +1,124 @@ +use anyhow::Result; +use imap_codec::imap_types::command::Command; +use imap_codec::imap_types::core::Tag; +use imap_codec::imap_types::response::{Code, Data, Status}; +use std::sync::Arc; +use tokio::sync::Notify; + +#[derive(Debug)] +pub enum Body<'a> { + Data(Data<'a>), + Status(Status<'a>), +} + +pub struct ResponseBuilder<'a> { + tag: Option>, + code: Option>, + text: String, + body: Vec>, +} + +impl<'a> ResponseBuilder<'a> { + pub fn to_req(mut self, cmd: &Command<'a>) -> Self { + self.tag = Some(cmd.tag.clone()); + self + } + pub fn tag(mut self, tag: Tag<'a>) -> Self { + self.tag = Some(tag); + self + } + + pub fn message(mut self, txt: impl Into) -> Self { + self.text = txt.into(); + self + } + + pub fn code(mut self, code: Code<'a>) -> Self { + self.code = Some(code); + self + } + + pub fn data(mut self, data: Data<'a>) -> Self { + self.body.push(Body::Data(data)); + self + } + + pub fn many_data(mut self, data: Vec>) -> Self { + for d in data.into_iter() { + self = self.data(d); + } + self + } + + #[allow(dead_code)] + pub fn info(mut self, status: Status<'a>) -> Self { + self.body.push(Body::Status(status)); + self + } + + #[allow(dead_code)] + pub fn many_info(mut self, status: Vec>) -> Self { + for d in status.into_iter() { + self = self.info(d); + } + self + } + + pub fn set_body(mut self, body: Vec>) -> Self { + self.body = body; + self + } + + pub fn ok(self) -> Result> { + Ok(Response { + completion: Status::ok(self.tag, self.code, self.text)?, + body: self.body, + }) + } + + pub fn no(self) -> Result> { + Ok(Response { + completion: Status::no(self.tag, self.code, self.text)?, + body: self.body, + }) + } + + pub fn bad(self) -> Result> { + Ok(Response { + completion: Status::bad(self.tag, self.code, self.text)?, + body: self.body, + }) + } +} + +#[derive(Debug)] +pub struct Response<'a> { + pub body: Vec>, + pub completion: Status<'a>, +} + +impl<'a> Response<'a> { + pub fn build() -> ResponseBuilder<'a> { + ResponseBuilder { + tag: None, + code: None, + text: "".to_string(), + body: vec![], + } + } + + pub fn bye() -> Result> { + Ok(Response { + completion: Status::bye(None, "bye")?, + body: vec![], + }) + } +} + +#[derive(Debug)] +pub enum ResponseOrIdle { + Response(Response<'static>), + IdleAccept(Arc), + IdleReject(Response<'static>), + IdleEvent(Vec>), +} diff --git a/aero-proto/src/imap/search.rs b/aero-proto/src/imap/search.rs new file mode 100644 index 0000000..3634a3a --- /dev/null +++ b/aero-proto/src/imap/search.rs @@ -0,0 +1,478 @@ +use std::num::{NonZeroU32, NonZeroU64}; + +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 aero_collections::mail::query::QueryScope; + +use crate::imap::index::MailIndex; +use crate::imap::mail_view::MailView; + +pub enum SeqType { + Undefined, + NonUid, + Uid, +} +impl SeqType { + pub fn is_uid(&self) -> bool { + matches!(self, Self::Uid) + } +} + +pub struct Criteria<'a>(pub &'a SearchKey<'a>); +impl<'a> Criteria<'a> { + /// Returns a set of email identifiers that is greater or equal + /// to the set of emails to return + pub fn to_sequence_set(&self) -> (SequenceSet, SeqType) { + match self.0 { + SearchKey::All => (sequence_set_all(), SeqType::Undefined), + SearchKey::SequenceSet(seq_set) => (seq_set.clone(), SeqType::NonUid), + SearchKey::Uid(seq_set) => (seq_set.clone(), SeqType::Uid), + SearchKey::Not(_inner) => { + tracing::debug!( + "using NOT in a search request is slow: it selects all identifiers" + ); + (sequence_set_all(), SeqType::Undefined) + } + SearchKey::Or(left, right) => { + tracing::debug!("using OR in a search request is slow: no deduplication is done"); + let (base, base_seqtype) = Self(&left).to_sequence_set(); + let (ext, ext_seqtype) = Self(&right).to_sequence_set(); + + // Check if we have a UID/ID conflict in fetching: now we don't know how to handle them + match (base_seqtype, ext_seqtype) { + (SeqType::Uid, SeqType::NonUid) | (SeqType::NonUid, SeqType::Uid) => { + (sequence_set_all(), SeqType::Undefined) + } + (SeqType::Undefined, x) | (x, _) => { + let mut new_vec = base.0.into_inner(); + new_vec.extend_from_slice(ext.0.as_ref()); + let seq = SequenceSet( + Vec1::try_from(new_vec) + .expect("merging non empty vec lead to non empty vec"), + ); + (seq, x) + } + } + } + SearchKey::And(search_list) => { + tracing::debug!( + "using AND in a search request is slow: no intersection is performed" + ); + // As we perform no intersection, we don't care if we mix uid or id. + // We only keep the smallest range, being it ID or UID, depending of + // which one has the less items. This is an approximation as UID ranges + // can have holes while ID ones can't. + search_list + .as_ref() + .iter() + .map(|crit| Self(&crit).to_sequence_set()) + .min_by(|(x, _), (y, _)| { + let x_size = approx_sequence_set_size(x); + let y_size = approx_sequence_set_size(y); + x_size.cmp(&y_size) + }) + .unwrap_or((sequence_set_all(), SeqType::Undefined)) + } + _ => (sequence_set_all(), SeqType::Undefined), + } + } + + /// Not really clever as we can have cases where we filter out + /// the email before needing to inspect its meta. + /// But for now we are seeking the most basic/stupid algorithm. + pub fn query_scope(&self) -> QueryScope { + use SearchKey::*; + match self.0 { + // Combinators + And(and_list) => and_list + .as_ref() + .iter() + .fold(QueryScope::Index, |prev, sk| { + prev.union(&Criteria(sk).query_scope()) + }), + Not(inner) => Criteria(inner).query_scope(), + Or(left, right) => Criteria(left) + .query_scope() + .union(&Criteria(right).query_scope()), + All => QueryScope::Index, + + // IMF Headers + Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) + | Subject(_) | To(_) => QueryScope::Partial, + // Internal Date is also stored in MailMeta + Before(_) | On(_) | Since(_) => QueryScope::Partial, + // Message size is also stored in MailMeta + Larger(_) | Smaller(_) => QueryScope::Partial, + // Text and Body require that we fetch the full content! + Text(_) | Body(_) => QueryScope::Full, + + _ => QueryScope::Index, + } + } + + pub fn is_modseq(&self) -> bool { + use SearchKey::*; + match self.0 { + And(and_list) => and_list + .as_ref() + .iter() + .any(|child| Criteria(child).is_modseq()), + Or(left, right) => Criteria(left).is_modseq() || Criteria(right).is_modseq(), + Not(child) => Criteria(child).is_modseq(), + ModSeq { .. } => true, + _ => false, + } + } + + /// Returns emails that we now for sure we want to keep + /// but also a second list of emails we need to investigate further by + /// fetching some remote data + pub fn filter_on_idx<'b>( + &self, + midx_list: &[&'b MailIndex<'b>], + ) -> (Vec<&'b MailIndex<'b>>, Vec<&'b MailIndex<'b>>) { + let (p1, p2): (Vec<_>, Vec<_>) = midx_list + .iter() + .map(|x| (x, self.is_keep_on_idx(x))) + .filter(|(_midx, decision)| decision.is_keep()) + .map(|(midx, decision)| (*midx, decision)) + .partition(|(_midx, decision)| matches!(decision, PartialDecision::Keep)); + + let to_keep = p1.into_iter().map(|(v, _)| v).collect(); + let to_fetch = p2.into_iter().map(|(v, _)| v).collect(); + (to_keep, to_fetch) + } + + // ---- + + /// Here we are doing a partial filtering: we do not have access + /// to the headers or to the body, so every time we encounter a rule + /// based on them, we need to keep it. + /// + /// @TODO Could be optimized on a per-email basis by also returning the QueryScope + /// when more information is needed! + fn is_keep_on_idx(&self, midx: &MailIndex) -> PartialDecision { + use SearchKey::*; + match self.0 { + // Combinator logic + And(expr_list) => expr_list + .as_ref() + .iter() + .fold(PartialDecision::Keep, |acc, cur| { + acc.and(&Criteria(cur).is_keep_on_idx(midx)) + }), + Or(left, right) => { + let left_decision = Criteria(left).is_keep_on_idx(midx); + let right_decision = Criteria(right).is_keep_on_idx(midx); + left_decision.or(&right_decision) + } + Not(expr) => Criteria(expr).is_keep_on_idx(midx).not(), + All => PartialDecision::Keep, + + // Sequence logic + maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, midx).into(), + maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, midx).into(), + ModSeq { + metadata_item, + modseq, + } => is_keep_modseq(metadata_item, modseq, midx).into(), + + // All the stuff we can't evaluate yet + Bcc(_) | Cc(_) | From(_) | Header(..) | SentBefore(_) | SentOn(_) | SentSince(_) + | Subject(_) | To(_) | Before(_) | On(_) | Since(_) | Larger(_) | Smaller(_) + | Text(_) | Body(_) => PartialDecision::Postpone, + + unknown => { + tracing::error!("Unknown filter {:?}", unknown); + PartialDecision::Discard + } + } + } + + /// @TODO we re-eveluate twice the same logic. The correct way would be, on each pass, + /// to simplify the searck query, by removing the elements that were already checked. + /// For example if we have AND(OR(seqid(X), body(Y)), body(X)), we can't keep for sure + /// 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. + pub fn is_keep_on_query(&self, mail_view: &MailView) -> bool { + use SearchKey::*; + match self.0 { + // Combinator logic + And(expr_list) => expr_list + .as_ref() + .iter() + .all(|cur| Criteria(cur).is_keep_on_query(mail_view)), + Or(left, right) => { + Criteria(left).is_keep_on_query(mail_view) + || Criteria(right).is_keep_on_query(mail_view) + } + Not(expr) => !Criteria(expr).is_keep_on_query(mail_view), + All => true, + + //@FIXME Reevaluating our previous logic... + maybe_seq if is_sk_seq(maybe_seq) => is_keep_seq(maybe_seq, &mail_view.in_idx), + maybe_flag if is_sk_flag(maybe_flag) => is_keep_flag(maybe_flag, &mail_view.in_idx), + ModSeq { + metadata_item, + modseq, + } => is_keep_modseq(metadata_item, modseq, &mail_view.in_idx).into(), + + // Filter on mail meta + Before(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive < search_naive.as_ref(), + _ => false, + }, + On(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive == search_naive.as_ref(), + _ => false, + }, + Since(search_naive) => match mail_view.stored_naive_date() { + Ok(msg_naive) => &msg_naive > search_naive.as_ref(), + _ => false, + }, + + // Message size is also stored in MailMeta + Larger(size_ref) => { + mail_view + .query_result + .metadata() + .expect("metadata were fetched") + .rfc822_size + > *size_ref as usize + } + Smaller(size_ref) => { + mail_view + .query_result + .metadata() + .expect("metadata were fetched") + .rfc822_size + < *size_ref as usize + } + + // Filter on well-known headers + Bcc(txt) => mail_view.is_header_contains_pattern(&b"bcc"[..], txt.as_ref()), + Cc(txt) => mail_view.is_header_contains_pattern(&b"cc"[..], txt.as_ref()), + From(txt) => mail_view.is_header_contains_pattern(&b"from"[..], txt.as_ref()), + Subject(txt) => mail_view.is_header_contains_pattern(&b"subject"[..], txt.as_ref()), + To(txt) => mail_view.is_header_contains_pattern(&b"to"[..], txt.as_ref()), + Header(hdr, txt) => mail_view.is_header_contains_pattern(hdr.as_ref(), txt.as_ref()), + + // Filter on Date header + SentBefore(search_naive) => mail_view + .imf() + .map(|imf| imf.naive_date().ok()) + .flatten() + .map(|msg_naive| &msg_naive < search_naive.as_ref()) + .unwrap_or(false), + SentOn(search_naive) => mail_view + .imf() + .map(|imf| imf.naive_date().ok()) + .flatten() + .map(|msg_naive| &msg_naive == search_naive.as_ref()) + .unwrap_or(false), + SentSince(search_naive) => mail_view + .imf() + .map(|imf| imf.naive_date().ok()) + .flatten() + .map(|msg_naive| &msg_naive > search_naive.as_ref()) + .unwrap_or(false), + + // Filter on the full content of the email + Text(txt) => mail_view + .content + .as_msg() + .map(|msg| { + msg.raw_part + .windows(txt.as_ref().len()) + .any(|win| win == txt.as_ref()) + }) + .unwrap_or(false), + Body(txt) => mail_view + .content + .as_msg() + .map(|msg| { + msg.raw_body + .windows(txt.as_ref().len()) + .any(|win| win == txt.as_ref()) + }) + .unwrap_or(false), + + unknown => { + tracing::error!("Unknown filter {:?}", unknown); + false + } + } + } +} + +// ---- Sequence things ---- +fn sequence_set_all() -> SequenceSet { + SequenceSet::from(Sequence::Range( + SeqOrUid::Value(NonZeroU32::MIN), + SeqOrUid::Asterisk, + )) +} + +// This is wrong as sequences can overlap +fn approx_sequence_set_size(seq_set: &SequenceSet) -> u64 { + seq_set.0.as_ref().iter().fold(0u64, |acc, seq| { + acc.saturating_add(approx_sequence_size(seq)) + }) +} + +// This is wrong as sequence UID can have holes, +// as we don't know the number of messages in the mailbox also +// we gave to guess +fn approx_sequence_size(seq: &Sequence) -> u64 { + match seq { + Sequence::Single(_) => 1, + Sequence::Range(SeqOrUid::Asterisk, _) | Sequence::Range(_, SeqOrUid::Asterisk) => u64::MAX, + Sequence::Range(SeqOrUid::Value(x1), SeqOrUid::Value(x2)) => { + let x2 = x2.get() as i64; + let x1 = x1.get() as i64; + (x2 - x1).abs().try_into().unwrap_or(1) + } + } +} + +// --- Partial decision things ---- + +enum PartialDecision { + Keep, + Discard, + Postpone, +} +impl From for PartialDecision { + fn from(x: bool) -> Self { + match x { + true => PartialDecision::Keep, + _ => PartialDecision::Discard, + } + } +} +impl PartialDecision { + fn not(&self) -> Self { + match self { + Self::Keep => Self::Discard, + Self::Discard => Self::Keep, + Self::Postpone => Self::Postpone, + } + } + + fn or(&self, other: &Self) -> Self { + match (self, other) { + (Self::Keep, _) | (_, Self::Keep) => Self::Keep, + (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, + (Self::Discard, Self::Discard) => Self::Discard, + } + } + + fn and(&self, other: &Self) -> Self { + match (self, other) { + (Self::Discard, _) | (_, Self::Discard) => Self::Discard, + (Self::Postpone, _) | (_, Self::Postpone) => Self::Postpone, + (Self::Keep, Self::Keep) => Self::Keep, + } + } + + fn is_keep(&self) -> bool { + !matches!(self, Self::Discard) + } +} + +// ----- Search Key things --- +fn is_sk_flag(sk: &SearchKey) -> bool { + use SearchKey::*; + match sk { + Answered | Deleted | Draft | Flagged | Keyword(..) | New | Old | Recent | Seen + | Unanswered | Undeleted | Undraft | Unflagged | Unkeyword(..) | Unseen => true, + _ => false, + } +} + +fn is_keep_flag(sk: &SearchKey, midx: &MailIndex) -> bool { + use SearchKey::*; + match sk { + Answered => midx.is_flag_set("\\Answered"), + Deleted => midx.is_flag_set("\\Deleted"), + Draft => midx.is_flag_set("\\Draft"), + Flagged => midx.is_flag_set("\\Flagged"), + Keyword(kw) => midx.is_flag_set(kw.inner()), + New => { + let is_recent = midx.is_flag_set("\\Recent"); + let is_seen = midx.is_flag_set("\\Seen"); + is_recent && !is_seen + } + Old => { + let is_recent = midx.is_flag_set("\\Recent"); + !is_recent + } + Recent => midx.is_flag_set("\\Recent"), + Seen => midx.is_flag_set("\\Seen"), + Unanswered => { + let is_answered = midx.is_flag_set("\\Recent"); + !is_answered + } + Undeleted => { + let is_deleted = midx.is_flag_set("\\Deleted"); + !is_deleted + } + Undraft => { + let is_draft = midx.is_flag_set("\\Draft"); + !is_draft + } + Unflagged => { + let is_flagged = midx.is_flag_set("\\Flagged"); + !is_flagged + } + Unkeyword(kw) => { + let is_keyword_set = midx.is_flag_set(kw.inner()); + !is_keyword_set + } + Unseen => { + let is_seen = midx.is_flag_set("\\Seen"); + !is_seen + } + + // Not flag logic + _ => unreachable!(), + } +} + +fn is_sk_seq(sk: &SearchKey) -> bool { + use SearchKey::*; + match sk { + SequenceSet(..) | Uid(..) => true, + _ => false, + } +} +fn is_keep_seq(sk: &SearchKey, midx: &MailIndex) -> bool { + use SearchKey::*; + match sk { + SequenceSet(seq_set) => seq_set + .0 + .as_ref() + .iter() + .any(|seq| midx.is_in_sequence_i(seq)), + Uid(seq_set) => seq_set + .0 + .as_ref() + .iter() + .any(|seq| midx.is_in_sequence_uid(seq)), + _ => unreachable!(), + } +} + +fn is_keep_modseq( + filter: &Option, + modseq: &NonZeroU64, + midx: &MailIndex, +) -> bool { + if filter.is_some() { + tracing::warn!(filter=?filter, "Ignoring search metadata filter as it's not supported yet"); + } + modseq <= &midx.modseq +} diff --git a/aero-proto/src/imap/session.rs b/aero-proto/src/imap/session.rs new file mode 100644 index 0000000..92b5eb6 --- /dev/null +++ b/aero-proto/src/imap/session.rs @@ -0,0 +1,175 @@ +use anyhow::{anyhow, bail, Context, Result}; +use imap_codec::imap_types::{command::Command, core::Tag}; + +use aero_user::login::ArcLoginProvider; + +use crate::imap::capability::{ClientCapability, ServerCapability}; +use crate::imap::command::{anonymous, authenticated, selected}; +use crate::imap::flow; +use crate::imap::request::Request; +use crate::imap::response::{Response, ResponseOrIdle}; + +//----- +pub struct Instance { + pub login_provider: ArcLoginProvider, + pub server_capabilities: ServerCapability, + pub client_capabilities: ClientCapability, + pub state: flow::State, +} +impl Instance { + pub fn new(login_provider: ArcLoginProvider, cap: ServerCapability) -> Self { + let client_cap = ClientCapability::new(&cap); + Self { + login_provider, + state: flow::State::NotAuthenticated, + server_capabilities: cap, + client_capabilities: client_cap, + } + } + + pub async fn request(&mut self, req: Request) -> ResponseOrIdle { + match req { + Request::IdleStart(tag) => self.idle_init(tag), + Request::IdlePoll => self.idle_poll().await, + Request::ImapCommand(cmd) => self.command(cmd).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"); + ResponseOrIdle::Response(Response::bye().unwrap()) + } + } + } + + pub async fn idle_poll_happy(&mut self) -> Result { + 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"), + }; + + tokio::select! { + _ = stop.notified() => { + self.state.apply(flow::Transition::UnIdle)?; + return Ok(ResponseOrIdle::Response(Response::build() + .tag(tag.clone()) + .message("IDLE completed") + .ok()?)) + }, + change = mbx.idle_sync() => { + tracing::debug!("idle event"); + return Ok(ResponseOrIdle::IdleEvent(change?)); + } + } + } + + pub async fn command(&mut self, cmd: Command<'static>) -> ResponseOrIdle { + // Command behavior is modulated by the state. + // To prevent state error, we handle the same command in separate code paths. + let (resp, tr) = match &mut self.state { + flow::State::NotAuthenticated => { + let ctx = anonymous::AnonymousContext { + req: &cmd, + login_provider: &self.login_provider, + server_capabilities: &self.server_capabilities, + }; + anonymous::dispatch(ctx).await + } + flow::State::Authenticated(ref user) => { + let ctx = authenticated::AuthenticatedContext { + req: &cmd, + server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, + user, + }; + authenticated::dispatch(ctx).await + } + flow::State::Selected(ref user, ref mut mailbox, ref perm) => { + let ctx = selected::SelectedContext { + req: &cmd, + server_capabilities: &self.server_capabilities, + client_capabilities: &mut self.client_capabilities, + user, + mailbox, + perm, + }; + selected::dispatch(ctx).await + } + flow::State::Idle(..) => Err(anyhow!("can not receive command while idling")), + flow::State::Logout => Response::build() + .tag(cmd.tag.clone()) + .message("No commands are allowed in the LOGOUT state.") + .bad() + .map(|r| (r, flow::Transition::None)), + } + .unwrap_or_else(|err| { + tracing::error!("Command error {:?} occured while processing {:?}", err, cmd); + ( + Response::build() + .to_req(&cmd) + .message("Internal error while processing command") + .bad() + .unwrap(), + flow::Transition::None, + ) + }); + + if let Err(e) = self.state.apply(tr) { + tracing::error!( + "Transition error {:?} occured while processing on command {:?}", + e, + cmd + ); + return ResponseOrIdle::Response(Response::build() + .to_req(&cmd) + .message( + "Internal error, processing command triggered an illegal IMAP state transition", + ) + .bad() + .unwrap()); + } + ResponseOrIdle::Response(resp) + + /*match &self.state { + flow::State::Idle(_, _, _, _, n) => ResponseOrIdle::StartIdle(n.clone()), + _ => ResponseOrIdle::Response(resp), + }*/ + } +} diff --git a/aero-proto/src/lib.rs b/aero-proto/src/lib.rs new file mode 100644 index 0000000..d5154cd --- /dev/null +++ b/aero-proto/src/lib.rs @@ -0,0 +1,6 @@ +#![feature(async_closure)] + +pub mod dav; +pub mod imap; +pub mod lmtp; +pub mod sasl; diff --git a/aero-proto/src/lmtp.rs b/aero-proto/src/lmtp.rs new file mode 100644 index 0000000..9d40296 --- /dev/null +++ b/aero-proto/src/lmtp.rs @@ -0,0 +1,219 @@ +use std::net::SocketAddr; +use std::{pin::Pin, sync::Arc}; + +use anyhow::Result; +use async_trait::async_trait; +use duplexify::Duplex; +use futures::{io, AsyncRead, AsyncReadExt, AsyncWrite}; +use futures::{ + stream, + stream::{FuturesOrdered, FuturesUnordered}, + StreamExt, +}; +use tokio::net::TcpListener; +use tokio::select; +use tokio::sync::watch; +use tokio_util::compat::*; +use smtp_message::{DataUnescaper, Email, EscapedDataReader, Reply, ReplyCode}; +use smtp_server::{reply, Config, ConnectionMetadata, Decision, MailMetadata}; + +use aero_user::config::*; +use aero_user::login::*; +use aero_collections::mail::incoming::EncryptedMessage; + +pub struct LmtpServer { + bind_addr: SocketAddr, + hostname: String, + login_provider: Arc, +} + +impl LmtpServer { + pub fn new( + config: LmtpConfig, + login_provider: Arc, + ) -> Arc { + Arc::new(Self { + bind_addr: config.bind_addr, + hostname: config.hostname, + login_provider, + }) + } + + pub async fn run(self: &Arc, mut must_exit: watch::Receiver) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + tracing::info!("LMTP server listening on {:#}", self.bind_addr); + + let mut connections = FuturesUnordered::new(); + + while !*must_exit.borrow() { + let wait_conn_finished = async { + if connections.is_empty() { + futures::future::pending().await + } else { + connections.next().await + } + }; + let (socket, remote_addr) = select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + tracing::info!("LMTP: accepted connection from {}", remote_addr); + + let conn = tokio::spawn(smtp_server::interact( + socket.compat(), + smtp_server::IsAlreadyTls::No, + (), + self.clone(), + )); + + connections.push(conn); + } + drop(tcp); + + tracing::info!("LMTP server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) + } +} + +// ---- + +pub struct Message { + to: Vec, +} + +#[async_trait] +impl Config for LmtpServer { + type Protocol = smtp_server::protocol::Lmtp; + + type ConnectionUserMeta = (); + type MailUserMeta = Message; + + fn hostname(&self, _conn_meta: &ConnectionMetadata<()>) -> &str { + &self.hostname + } + + async fn new_mail(&self, _conn_meta: &mut ConnectionMetadata<()>) -> Message { + Message { to: vec![] } + } + + async fn tls_accept( + &self, + _io: IO, + _conn_meta: &mut ConnectionMetadata<()>, + ) -> io::Result>, Pin>>> + where + IO: Send + AsyncRead + AsyncWrite, + { + Err(io::Error::new( + io::ErrorKind::InvalidInput, + "TLS not implemented for LMTP server", + )) + } + + async fn filter_from( + &self, + from: Option, + _meta: &mut MailMetadata, + _conn_meta: &mut ConnectionMetadata<()>, + ) -> Decision> { + Decision::Accept { + reply: reply::okay_from().convert(), + res: from, + } + } + + async fn filter_to( + &self, + to: Email, + meta: &mut MailMetadata, + _conn_meta: &mut ConnectionMetadata<()>, + ) -> Decision { + let to_str = match to.hostname.as_ref() { + Some(h) => format!("{}@{}", to.localpart, h), + None => to.localpart.to_string(), + }; + match self.login_provider.public_login(&to_str).await { + Ok(creds) => { + meta.user.to.push(creds); + Decision::Accept { + reply: reply::okay_to().convert(), + res: to, + } + } + Err(e) => Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], + }, + }, + } + } + + async fn handle_mail<'resp, R>( + &'resp self, + reader: &mut EscapedDataReader<'_, R>, + meta: MailMetadata, + _conn_meta: &'resp mut ConnectionMetadata<()>, + ) -> Pin> + Send + 'resp>> + where + R: Send + Unpin + AsyncRead, + { + let err_response_stream = |meta: MailMetadata, msg: String| { + Box::pin( + stream::iter(meta.user.to.into_iter()).map(move |_| Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(msg.clone())], + }, + }), + ) + }; + + let mut text = Vec::new(); + if let Err(e) = reader.read_to_end(&mut text).await { + 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), + Err(e) => return err_response_stream(meta, e.to_string()), + }; + + Box::pin( + meta.user + .to + .into_iter() + .map(move |creds| { + let encrypted_message = encrypted_message.clone(); + async move { + match encrypted_message.deliver_to(creds).await { + Ok(()) => Decision::Accept { + reply: reply::okay_mail().convert(), + res: (), + }, + Err(e) => Decision::Reject { + reply: Reply { + code: ReplyCode::POLICY_REASON, + ecode: None, + text: vec![smtp_message::MaybeUtf8::Utf8(e.to_string())], + }, + }, + } + } + }) + .collect::>(), + ) + } +} diff --git a/aero-proto/src/sasl.rs b/aero-proto/src/sasl.rs new file mode 100644 index 0000000..dae89eb --- /dev/null +++ b/aero-proto/src/sasl.rs @@ -0,0 +1,142 @@ +use std::net::SocketAddr; + +use anyhow::{anyhow, bail, Result}; +use futures::stream::{FuturesUnordered, StreamExt}; +use tokio::io::BufStream; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, TcpStream}; +use tokio::sync::watch; +use tokio_util::bytes::BytesMut; + +use aero_user::config::AuthConfig; +use aero_user::login::ArcLoginProvider; +use aero_sasl::{flow::State, decode::client_command, encode::Encode}; + +pub struct AuthServer { + login_provider: ArcLoginProvider, + bind_addr: SocketAddr, +} + +impl AuthServer { + 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) -> Result<()> { + let tcp = TcpListener::bind(self.bind_addr).await?; + tracing::info!( + "SASL Authentication Protocol listening on {:#}", + self.bind_addr + ); + + let mut connections = FuturesUnordered::new(); + + while !*must_exit.borrow() { + let wait_conn_finished = async { + if connections.is_empty() { + futures::future::pending().await + } else { + connections.next().await + } + }; + + let (socket, remote_addr) = tokio::select! { + a = tcp.accept() => a?, + _ = wait_conn_finished => continue, + _ = must_exit.changed() => continue, + }; + + tracing::info!("AUTH: accepted connection from {}", remote_addr); + let conn = tokio::spawn( + NetLoop::new(socket, self.login_provider.clone(), must_exit.clone()).run_error(), + ); + + connections.push(conn); + } + drop(tcp); + + tracing::info!("AUTH server shutting down, draining remaining connections..."); + while connections.next().await.is_some() {} + + Ok(()) + } +} + +struct NetLoop { + login: ArcLoginProvider, + stream: BufStream, + stop: watch::Receiver, + state: State, + read_buf: Vec, + write_buf: BytesMut, +} + +impl NetLoop { + fn new(stream: TcpStream, login: ArcLoginProvider, stop: watch::Receiver) -> Self { + Self { + login, + stream: BufStream::new(stream), + state: State::Init, + stop, + read_buf: Vec::new(), + write_buf: BytesMut::new(), + } + } + + async fn run_error(self) { + match self.run().await { + Ok(()) => tracing::info!("Auth session succeeded"), + Err(e) => tracing::error!(err=?e, "Auth session failed"), + } + } + + async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + read_res = self.stream.read_until(b'\n', &mut self.read_buf) => { + // Detect EOF / socket close + let bread = read_res?; + if bread == 0 { + tracing::info!("Reading buffer empty, connection has been closed. Exiting AUTH session."); + return Ok(()) + } + + // Parse command + let (_, cmd) = client_command(&self.read_buf).map_err(|_| anyhow!("Unable to parse command"))?; + tracing::trace!(cmd=?cmd, "Received command"); + + // Make some progress in our local state + let login = async |user: String, pass: String| self.login.login(user.as_str(), pass.as_str()).await.is_ok(); + self.state.progress(cmd, login).await; + if matches!(self.state, State::Error) { + bail!("Internal state is in error, previous logs explain what went wrong"); + } + + // Build response + let srv_cmds = self.state.response(); + srv_cmds.iter().try_for_each(|r| { + tracing::trace!(cmd=?r, "Sent command"); + r.encode(&mut self.write_buf) + })?; + + // Send responses if at least one command response has been generated + if !srv_cmds.is_empty() { + self.stream.write_all(&self.write_buf).await?; + self.stream.flush().await?; + } + + // Reset buffers + self.read_buf.clear(); + self.write_buf.clear(); + }, + _ = self.stop.changed() => { + tracing::debug!("Server is stopping, quitting this runner"); + return Ok(()) + } + } + } + } +} diff --git a/aero-sasl/src/flow.rs b/aero-sasl/src/flow.rs index 6cc698a..31c8bc5 100644 --- a/aero-sasl/src/flow.rs +++ b/aero-sasl/src/flow.rs @@ -28,9 +28,9 @@ impl State { Self::Init } - async fn try_auth_plain<'a, X, F>(&self, data: &'a [u8], login: X) -> AuthRes + async fn try_auth_plain(&self, data: &[u8], login: X) -> AuthRes where - X: FnOnce(&'a str, &'a str) -> F, + X: FnOnce(String, String) -> F, F: Future, { // Check that we can extract user's login+pass @@ -56,7 +56,7 @@ impl State { }; // Try to connect user - match login(user, password).await { + match login(user.to_string(), password.to_string()).await { true => AuthRes::Success(user.to_string()), false => { tracing::warn!("login failed"); @@ -67,7 +67,7 @@ impl State { pub async fn progress(&mut self, cmd: ClientCommand, login: X) where - X: FnOnce(&str, &str) -> F, + X: FnOnce(String, String) -> F, F: Future, { let new_state = 'state: { -- cgit v1.2.3