mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 13:17:55 +08:00
feat: init jmap support
This commit is contained in:
Generated
+101
-137
@@ -26,21 +26,6 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse 0.2.7",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
@@ -48,7 +33,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse 1.0.0",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
@@ -62,15 +47,6 @@ version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
@@ -123,9 +99,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-rs"
|
||||
version = "1.16.1"
|
||||
version = "1.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf"
|
||||
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||
dependencies = [
|
||||
"aws-lc-sys",
|
||||
"zeroize",
|
||||
@@ -133,9 +109,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aws-lc-sys"
|
||||
version = "0.38.0"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e"
|
||||
checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cmake",
|
||||
@@ -257,7 +233,7 @@ version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream 1.0.0",
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
@@ -443,9 +419,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f"
|
||||
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
|
||||
dependencies = [
|
||||
"log",
|
||||
"regex",
|
||||
@@ -453,11 +429,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.11.9"
|
||||
version = "0.11.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d"
|
||||
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
|
||||
dependencies = [
|
||||
"anstream 0.6.21",
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"env_filter",
|
||||
"jiff",
|
||||
@@ -634,6 +610,7 @@ dependencies = [
|
||||
"gethostname",
|
||||
"io-fs",
|
||||
"io-imap",
|
||||
"io-jmap",
|
||||
"io-maildir",
|
||||
"io-process",
|
||||
"io-smtp",
|
||||
@@ -646,11 +623,28 @@ dependencies = [
|
||||
"rfc2047-decoder",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shellexpand",
|
||||
"uds_windows",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -802,16 +796,26 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "io-fs"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-fs#6c2305c52fdd5ec9ed05e902183fdf2942ea0590"
|
||||
dependencies = [
|
||||
"log",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-http"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"http",
|
||||
"httparse",
|
||||
"io-stream",
|
||||
"log",
|
||||
"memchr",
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-imap"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-imap?branch=io#35186c5ea0f6c535a528029e22ebb6a02bff8473"
|
||||
dependencies = [
|
||||
"imap-codec",
|
||||
"io-stream",
|
||||
@@ -821,10 +825,24 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-jmap"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"http",
|
||||
"io-http",
|
||||
"io-stream",
|
||||
"log",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.18",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "io-maildir"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-maildir#105acf1b5e2e005c0afea4ecb27ebe2ae49d3e4f"
|
||||
dependencies = [
|
||||
"gethostname",
|
||||
"io-fs",
|
||||
@@ -851,7 +869,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "io-smtp"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/pimalaya/io-smtp#3a74933b9c6e98723c31c6f8a68b366657ccb395"
|
||||
dependencies = [
|
||||
"io-stream",
|
||||
"log",
|
||||
@@ -898,9 +915,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
@@ -935,7 +952,7 @@ dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
@@ -944,9 +961,31 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.3.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
|
||||
dependencies = [
|
||||
"jni-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||
dependencies = [
|
||||
"jni-sys-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-sys-macros"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
@@ -994,9 +1033,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a"
|
||||
checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
@@ -1259,9 +1298,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
[[package]]
|
||||
name = "pimalaya-toolbox"
|
||||
version = "0.0.4"
|
||||
source = "git+https://github.com/pimalaya/toolbox#64babb1b4358dc57910d815fa91f0dbae9423a9c"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"clap",
|
||||
"clap_complete",
|
||||
"clap_mangen",
|
||||
@@ -1270,6 +1309,7 @@ dependencies = [
|
||||
"gethostname",
|
||||
"git2",
|
||||
"io-imap",
|
||||
"io-jmap",
|
||||
"io-process",
|
||||
"io-smtp",
|
||||
"io-stream",
|
||||
@@ -1367,9 +1407,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quoted_printable"
|
||||
version = "0.5.1"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c9bd8497b02465aeef5375144c26062e0dcd5939dfcbb0f5db76cb8c17c73"
|
||||
checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
@@ -1592,9 +1632,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.103.9"
|
||||
version = "0.103.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53"
|
||||
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||
dependencies = [
|
||||
"aws-lc-rs",
|
||||
"ring",
|
||||
@@ -1756,7 +1796,6 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
[[package]]
|
||||
name = "smtp-codec"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/pimalaya/smtp-codec#baeb185ad0a63d43c48b05f7f7a4d9e81b291332"
|
||||
dependencies = [
|
||||
"abnf-core",
|
||||
"base64",
|
||||
@@ -1768,7 +1807,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "smtp-types"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/pimalaya/smtp-codec#baeb185ad0a63d43c48b05f7f7a4d9e81b291332"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bounded-static",
|
||||
@@ -1844,12 +1882,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.4.3"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0"
|
||||
checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
|
||||
dependencies = [
|
||||
"rustix",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1968,9 +2006,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
checksum = "da36089a805484bcccfffe0739803392c8298778a2d2f09febf76fac5ad9025b"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
@@ -2218,15 +2256,6 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -2260,30 +2289,13 @@ dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm 0.53.1",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -2296,12 +2308,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -2314,12 +2320,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -2332,24 +2332,12 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -2362,12 +2350,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
@@ -2380,12 +2362,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
@@ -2398,12 +2374,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
@@ -2416,12 +2386,6 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
@@ -2550,18 +2514,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.42"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.42"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
+6
-1
@@ -16,9 +16,10 @@ all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["imap", "smtp", "maildir", "rustls-ring"]
|
||||
default = ["imap", "maildir", "smtp", "rustls-ring"]
|
||||
|
||||
imap = ["dep:io-imap", "dep:rfc2047-decoder", "pimalaya-toolbox/imap"]
|
||||
jmap = ["dep:io-jmap", "dep:serde_json", "pimalaya-toolbox/jmap"]
|
||||
smtp = ["dep:io-smtp", "pimalaya-toolbox/smtp"]
|
||||
maildir = ["dep:convert_case", "dep:io-fs", "dep:io-maildir", "dep:mime_guess"]
|
||||
|
||||
@@ -41,6 +42,7 @@ dirs = "6"
|
||||
gethostname = "1"
|
||||
io-fs = { version = "0.0.1", default-features = false, features = ["std"], optional = true }
|
||||
io-imap = { version = "0.0.1", default-features = false, optional = true }
|
||||
io-jmap = { version = "0.0.1", default-features = false, optional = true }
|
||||
io-maildir = { version = "0.0.1", default-features = false, features = ["serde"], optional = true }
|
||||
io-process = { version = "0.0.2", default-features = false }
|
||||
io-smtp = { version = "0.0.1", default-features = false, features = ["ext_auth", "starttls"], optional = true }
|
||||
@@ -53,6 +55,7 @@ pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["c
|
||||
rfc2047-decoder = { version = "1", optional = true }
|
||||
secrecy = "0.10"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", optional = true }
|
||||
shellexpand = "3.1"
|
||||
url = { version = "2.2", features = ["serde"] }
|
||||
|
||||
@@ -62,6 +65,8 @@ uds_windows = "1"
|
||||
[patch.crates-io]
|
||||
io-fs.git = "https://github.com/pimalaya/io-fs"
|
||||
io-imap = { git = "https://github.com/pimalaya/io-imap", branch = "io" }
|
||||
io-jmap.git = "https://github.com/pimalaya/io-jmap"
|
||||
io-http.git = "https://github.com/pimalaya/io-http"
|
||||
io-maildir.git = "https://github.com/pimalaya/io-maildir"
|
||||
io-smtp.git = "https://github.com/pimalaya/io-smtp"
|
||||
pimalaya-toolbox.git = "https://github.com/pimalaya/toolbox"
|
||||
|
||||
+18
@@ -17,6 +17,8 @@ use pimalaya_toolbox::{
|
||||
|
||||
#[cfg(feature = "imap")]
|
||||
use crate::imap::command::ImapCommand;
|
||||
#[cfg(feature = "jmap")]
|
||||
use crate::jmap::command::JmapCommand;
|
||||
#[cfg(feature = "maildir")]
|
||||
use crate::maildir::command::MaildirCommand;
|
||||
#[cfg(feature = "smtp")]
|
||||
@@ -61,6 +63,9 @@ pub enum BackendCommand {
|
||||
#[cfg(feature = "imap")]
|
||||
#[command(subcommand)]
|
||||
Imap(ImapCommand),
|
||||
#[cfg(feature = "jmap")]
|
||||
#[command(subcommand)]
|
||||
Jmap(JmapCommand),
|
||||
#[cfg(feature = "maildir")]
|
||||
#[command(subcommand)]
|
||||
Maildir(MaildirCommand),
|
||||
@@ -93,6 +98,19 @@ impl BackendCommand {
|
||||
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
#[cfg(feature = "jmap")]
|
||||
Self::Jmap(cmd) => {
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
let (account_name, mut account_config) = config.get_account(account_name)?;
|
||||
|
||||
let Some(jmap_config) = account_config.jmap.take() else {
|
||||
bail!("JMAP config is missing for account `{account_name}`")
|
||||
};
|
||||
|
||||
let account = Account::new(config, account_config, jmap_config)?;
|
||||
|
||||
cmd.execute(printer, account)
|
||||
}
|
||||
#[cfg(feature = "maildir")]
|
||||
Self::Maildir(cmd) => {
|
||||
let config = Config::from_paths_or_default(config_paths)?;
|
||||
|
||||
@@ -54,8 +54,12 @@ pub struct AccountConfig {
|
||||
pub table_preset: Option<String>,
|
||||
pub table_arrangement: Option<TableArrangementConfig>,
|
||||
|
||||
#[allow(unused)]
|
||||
pub imap: Option<ImapConfig>,
|
||||
pub jmap: Option<JmapConfig>,
|
||||
#[allow(unused)]
|
||||
pub maildir: Option<MaildirConfig>,
|
||||
#[allow(unused)]
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
}
|
||||
|
||||
@@ -79,6 +83,7 @@ impl From<TableArrangementConfig> for ContentArrangement {
|
||||
}
|
||||
|
||||
/// IMAP configuration.
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct ImapConfig {
|
||||
@@ -92,6 +97,7 @@ pub struct ImapConfig {
|
||||
}
|
||||
|
||||
/// Maildir configuration.
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct MaildirConfig {
|
||||
@@ -99,6 +105,7 @@ pub struct MaildirConfig {
|
||||
}
|
||||
|
||||
/// SMTP configuration.
|
||||
#[allow(unused)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct SmtpConfig {
|
||||
@@ -255,3 +262,51 @@ impl TryFrom<SaslConfig> for Sasl {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// JMAP configuration.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub struct JmapConfig {
|
||||
/// The HTTPS base URL of the JMAP server.
|
||||
///
|
||||
/// Must use the `https://` or `jmap://` scheme. Session discovery
|
||||
/// (`GET /.well-known/jmap`) is performed automatically on connection.
|
||||
pub url: Url,
|
||||
|
||||
/// TLS configuration.
|
||||
#[serde(default)]
|
||||
pub tls: TlsConfig,
|
||||
|
||||
/// Authentication configuration.
|
||||
pub auth: JmapAuthConfig,
|
||||
}
|
||||
|
||||
/// JMAP authentication configuration.
|
||||
// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
|
||||
pub enum JmapAuthConfig {
|
||||
/// Bearer token (OAuth 2.0 access token).
|
||||
Bearer { token: Secret },
|
||||
/// HTTP Basic authentication (username + password).
|
||||
Basic {
|
||||
#[serde(deserialize_with = "shell_expanded_string")]
|
||||
username: String,
|
||||
password: Secret,
|
||||
},
|
||||
}
|
||||
|
||||
#[cfg(feature = "jmap")]
|
||||
impl TryFrom<JmapAuthConfig> for pimalaya_toolbox::stream::jmap::JmapAuth {
|
||||
type Error = pimalaya_toolbox::secret::SecretError;
|
||||
|
||||
fn try_from(config: JmapAuthConfig) -> Result<Self, Self::Error> {
|
||||
match config {
|
||||
JmapAuthConfig::Bearer { token } => Ok(Self::Bearer(token.get()?)),
|
||||
JmapAuthConfig::Basic { username, password } => Ok(Self::Basic {
|
||||
username,
|
||||
password: password.get()?,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
use anyhow::Result;
|
||||
use pimalaya_toolbox::stream::jmap::JmapSession;
|
||||
|
||||
use crate::{account::Account, config::JmapConfig};
|
||||
|
||||
pub type JmapAccount = Account<JmapConfig>;
|
||||
|
||||
impl JmapAccount {
|
||||
pub fn new_jmap_session(&self) -> Result<JmapSession> {
|
||||
JmapSession::new(
|
||||
self.backend.url.clone(),
|
||||
self.backend.tls.clone().try_into()?,
|
||||
self.backend.auth.clone().try_into()?,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount, email::command::JmapEmailCommand, identity::command::IdentityCommand,
|
||||
mailbox::command::JmapMailboxCommand, query::QueryCommand,
|
||||
submission::command::SubmissionCommand, thread::command::ThreadCommand,
|
||||
vacation::command::VacationCommand,
|
||||
};
|
||||
|
||||
/// JMAP CLI (requires the `jmap` cargo feature).
|
||||
///
|
||||
/// This command gives you access to the JMAP CLI API, and allows you
|
||||
/// to manage JMAP mailboxes, threads, emails, identities, submissions
|
||||
/// and vacation responses.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum JmapCommand {
|
||||
#[command(subcommand)]
|
||||
#[command(visible_aliases = ["mbox"])]
|
||||
Mailboxes(JmapMailboxCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
#[command(visible_aliases = ["msg"])]
|
||||
Emails(JmapEmailCommand),
|
||||
|
||||
#[command(subcommand)]
|
||||
Threads(ThreadCommand),
|
||||
#[command(subcommand)]
|
||||
#[command(aliases = ["identities"])]
|
||||
Identity(IdentityCommand),
|
||||
#[command(subcommand)]
|
||||
#[command(aliases = ["submissions", "submit"])]
|
||||
Submission(SubmissionCommand),
|
||||
#[command(subcommand)]
|
||||
#[command(alias = "vacation-response")]
|
||||
Vacation(VacationCommand),
|
||||
Query(QueryCommand),
|
||||
}
|
||||
|
||||
impl JmapCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Mailboxes(cmd) => cmd.execute(printer, account),
|
||||
Self::Emails(cmd) => cmd.execute(printer, account),
|
||||
|
||||
Self::Threads(cmd) => cmd.execute(printer, account),
|
||||
Self::Identity(cmd) => cmd.execute(printer, account),
|
||||
Self::Submission(cmd) => cmd.execute(printer, account),
|
||||
Self::Vacation(cmd) => cmd.execute(printer, account),
|
||||
Self::Query(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
email::{
|
||||
copy::CopyEmailCommand, delete::DeleteEmailCommand, get::JmapEmailGetCommand,
|
||||
import::ImportEmailCommand, parse::ParseEmailCommand, query::JmapEmailQueryCommand,
|
||||
update::JmapEmailUpdateCommand,
|
||||
},
|
||||
};
|
||||
|
||||
/// Manage JMAP emails.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum JmapEmailCommand {
|
||||
Get(JmapEmailGetCommand),
|
||||
Query(JmapEmailQueryCommand),
|
||||
#[command(alias = "edit")]
|
||||
Update(JmapEmailUpdateCommand),
|
||||
#[command(aliases = ["remove", "rm"])]
|
||||
Delete(DeleteEmailCommand),
|
||||
Copy(CopyEmailCommand),
|
||||
Import(ImportEmailCommand),
|
||||
Parse(ParseEmailCommand),
|
||||
}
|
||||
|
||||
impl JmapEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Query(cmd) => cmd.execute(printer, account),
|
||||
Self::Update(cmd) => cmd.execute(printer, account),
|
||||
Self::Delete(cmd) => cmd.execute(printer, account),
|
||||
Self::Copy(cmd) => cmd.execute(printer, account),
|
||||
Self::Import(cmd) => cmd.execute(printer, account),
|
||||
Self::Parse(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_copy::{CopyJmapEmails, CopyJmapEmailsResult},
|
||||
types::email::EmailCopy,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Copy JMAP emails from another account (Email/copy).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CopyEmailCommand {
|
||||
/// Email ID(s) to copy.
|
||||
#[arg(value_name = "EMAIL-ID", required = true, num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Source account ID to copy from.
|
||||
#[arg(long, value_name = "ACCOUNT-ID")]
|
||||
pub from_account: String,
|
||||
|
||||
/// Destination mailbox ID(s) to place copies in.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
pub mailbox_id: Vec<String>,
|
||||
}
|
||||
|
||||
impl CopyEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mailbox_ids: HashMap<String, bool> =
|
||||
self.mailbox_id.iter().map(|m| (m.clone(), true)).collect();
|
||||
|
||||
let emails: HashMap<String, EmailCopy> = self
|
||||
.ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
(
|
||||
id.clone(),
|
||||
EmailCopy {
|
||||
id: id.clone(),
|
||||
mailbox_ids: mailbox_ids.clone(),
|
||||
keywords: None,
|
||||
received_at: None,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut coroutine = CopyJmapEmails::new(jmap.context, self.from_account.clone(), emails)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
CopyJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
CopyJmapEmailsResult::Ok { not_created, .. } => break not_created,
|
||||
CopyJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_created {
|
||||
let mut ctx = anyhow!("Failed to copy email `{id}`");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully copied"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Delete JMAP emails (Email/set destroy).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DeleteEmailCommand {
|
||||
/// Email ID(s) to delete.
|
||||
#[arg(value_name = "ID", required = true, num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl DeleteEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in self.ids {
|
||||
args.destroy(id);
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_destroyed = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_destroyed, .. } => break not_destroyed,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_destroyed {
|
||||
let mut ctx = anyhow!("Failed to delete email `{id}`");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully deleted"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_get::{GetJmapEmails, GetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Get a JMAP email by ID (Email/get).
|
||||
///
|
||||
/// Downloads and displays the full message content including body.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapEmailGetCommand {
|
||||
/// The email ID(s) to retrieve.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Output raw RFC 5322 message headers.
|
||||
#[arg(long, short)]
|
||||
pub raw: bool,
|
||||
}
|
||||
|
||||
impl JmapEmailGetCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine =
|
||||
GetJmapEmails::new(jmap.context, self.ids.clone(), None, true, true, None)?;
|
||||
let mut arg = None;
|
||||
|
||||
let (emails, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapEmailsResult::Ok {
|
||||
emails, not_found, ..
|
||||
} => break (emails, not_found),
|
||||
GetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in not_found {
|
||||
warn!("email `{id}` not found");
|
||||
}
|
||||
|
||||
for email in emails {
|
||||
if self.raw {
|
||||
if let Some(headers) = &email.headers {
|
||||
for h in headers {
|
||||
printer.log(format!("{}: {}", h.name, h.value))?;
|
||||
}
|
||||
}
|
||||
printer.log("")?;
|
||||
}
|
||||
|
||||
if let Some(body_values) = &email.body_values {
|
||||
if let Some(text_parts) = &email.text_body {
|
||||
for part in text_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
printer.out(&body_value.value)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(html_parts) = &email.html_body {
|
||||
for part in html_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
printer.out(&body_value.value)?;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_import::{ImportJmapEmail, ImportJmapEmailResult},
|
||||
types::email::EmailImport,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Import an RFC 5322 message blob into a mailbox (Email/import).
|
||||
///
|
||||
/// The blob must already be uploaded to the JMAP server.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ImportEmailCommand {
|
||||
/// Blob ID of the RFC 5322 message to import.
|
||||
#[arg(value_name = "BLOB-ID")]
|
||||
pub blob_id: String,
|
||||
|
||||
/// Mailbox ID(s) to place the imported email in.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
pub mailbox_id: Vec<String>,
|
||||
|
||||
/// Keywords to set on the imported email (e.g. `$seen`).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
pub keyword: Vec<String>,
|
||||
|
||||
/// Override the `receivedAt` time (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub received_at: Option<String>,
|
||||
}
|
||||
|
||||
impl ImportEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mailbox_ids: HashMap<String, bool> =
|
||||
self.mailbox_id.iter().map(|m| (m.clone(), true)).collect();
|
||||
|
||||
let keywords = if self.keyword.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.keyword.iter().map(|kw| (kw.clone(), true)).collect())
|
||||
};
|
||||
|
||||
let import = EmailImport {
|
||||
blob_id: self.blob_id.clone(),
|
||||
mailbox_ids,
|
||||
keywords,
|
||||
received_at: self.received_at,
|
||||
};
|
||||
|
||||
let mut emails = HashMap::new();
|
||||
emails.insert(self.blob_id.clone(), import);
|
||||
|
||||
let mut coroutine = ImportJmapEmail::new(jmap.context, emails)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ImportJmapEmailResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
ImportJmapEmailResult::Ok { not_created, .. } => break not_created,
|
||||
ImportJmapEmailResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.blob_id) {
|
||||
let mut ctx = anyhow!("Failed to import email from blob `{}`", self.blob_id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email successfully imported from blob"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
pub mod command;
|
||||
pub mod copy;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod import;
|
||||
pub mod parse;
|
||||
pub mod query;
|
||||
pub mod update;
|
||||
@@ -0,0 +1,68 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_parse::{ParseJmapEmails, ParseJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Parse RFC 5322 message blobs without storing them (Email/parse).
|
||||
///
|
||||
/// Useful for reading attached .eml files or message blobs that are
|
||||
/// not yet stored as Email objects.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ParseEmailCommand {
|
||||
/// Blob ID(s) to parse as RFC 5322 messages.
|
||||
#[arg(value_name = "BLOB-ID", required = true, num_args = 1..)]
|
||||
pub blob_ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl ParseEmailCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine = ParseJmapEmails::new(jmap.context, self.blob_ids.clone(), None)?;
|
||||
let mut arg = None;
|
||||
|
||||
let (parsed, not_parsable, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
ParseJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
ParseJmapEmailsResult::Ok {
|
||||
context,
|
||||
parsed,
|
||||
not_parsable,
|
||||
not_found,
|
||||
..
|
||||
} => {
|
||||
jmap.context = context;
|
||||
break (parsed, not_parsable, not_found);
|
||||
}
|
||||
ParseJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Blob `{id}` not found."))?;
|
||||
}
|
||||
|
||||
for id in ¬_parsable {
|
||||
printer.log(format!("Blob `{id}` is not a valid RFC 5322 message."))?;
|
||||
}
|
||||
|
||||
for (_blob_id, email) in parsed {
|
||||
if let Some(body_values) = &email.body_values {
|
||||
if let Some(text_parts) = &email.text_body {
|
||||
for part in text_parts {
|
||||
if let Some(part_id) = &part.part_id {
|
||||
if let Some(body_value) = body_values.get(part_id) {
|
||||
printer.out(&body_value.value)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{Cell, ContentArrangement, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::email_query::{QueryJmapEmails, QueryJmapEmailsResult},
|
||||
types::email::{Email, EmailAddress, EmailComparator, EmailFilter, EmailSortProperty},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Query JMAP emails (Email/query + Email/get).
|
||||
///
|
||||
/// Lists, filters and sorts email envelopes.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapEmailQueryCommand {
|
||||
/// Filter by mailbox ID.
|
||||
#[arg(long, short, value_name = "MAILBOX-ID")]
|
||||
pub mailbox: Option<String>,
|
||||
|
||||
/// Filter by received-before date (RFC 3339, e.g. 2024-01-01T00:00:00Z).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub before: Option<String>,
|
||||
|
||||
/// Filter by received-after date (RFC 3339, e.g. 2024-01-01T00:00:00Z).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub after: Option<String>,
|
||||
|
||||
/// Filter by minimum size in bytes.
|
||||
#[arg(long, value_name = "BYTES")]
|
||||
pub min_size: Option<u64>,
|
||||
|
||||
/// Filter by maximum size in bytes.
|
||||
#[arg(long, value_name = "BYTES")]
|
||||
pub max_size: Option<u64>,
|
||||
|
||||
/// Filter to emails that have this keyword set.
|
||||
#[arg(long, value_name = "KEYWORD")]
|
||||
pub has_keyword: Option<String>,
|
||||
|
||||
/// Filter to emails that do not have this keyword set.
|
||||
#[arg(long, value_name = "KEYWORD")]
|
||||
pub not_keyword: Option<String>,
|
||||
|
||||
/// Filter to emails that have at least one attachment.
|
||||
#[arg(long)]
|
||||
pub has_attachment: bool,
|
||||
|
||||
/// Full-text search across all headers and body.
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub text: Option<String>,
|
||||
|
||||
/// Filter by From header (substring match).
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub from: Option<String>,
|
||||
|
||||
/// Filter by To header (substring match).
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub to: Option<String>,
|
||||
|
||||
/// Filter by Subject header (substring match).
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Filter by email body (substring match).
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub body: Option<String>,
|
||||
|
||||
/// Sort by property.
|
||||
#[arg(long, value_name = "PROP", default_value_t)]
|
||||
pub sort: SortArg,
|
||||
|
||||
/// Sort in descending order.
|
||||
#[arg(long, default_value_t)]
|
||||
pub desc: bool,
|
||||
|
||||
/// Number of emails to display per page.
|
||||
#[arg(long, short = 's', value_name = "N", default_value = "10")]
|
||||
pub page_size: u64,
|
||||
|
||||
/// Page index, starting from 1.
|
||||
#[arg(long, short, value_name = "N", default_value = "1")]
|
||||
pub page: u64,
|
||||
}
|
||||
|
||||
impl JmapEmailQueryCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let filter = {
|
||||
let f = EmailFilter {
|
||||
in_mailbox: self.mailbox,
|
||||
before: self.before,
|
||||
after: self.after,
|
||||
min_size: self.min_size,
|
||||
max_size: self.max_size,
|
||||
has_keyword: self.has_keyword,
|
||||
not_keyword: self.not_keyword,
|
||||
has_attachment: if self.has_attachment {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
text: self.text,
|
||||
from: self.from,
|
||||
to: self.to,
|
||||
subject: self.subject,
|
||||
body: self.body,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let has_one_filter = f.in_mailbox.is_some()
|
||||
|| f.before.is_some()
|
||||
|| f.after.is_some()
|
||||
|| f.min_size.is_some()
|
||||
|| f.max_size.is_some()
|
||||
|| f.has_keyword.is_some()
|
||||
|| f.not_keyword.is_some()
|
||||
|| f.has_attachment.is_some()
|
||||
|| f.text.is_some()
|
||||
|| f.from.is_some()
|
||||
|| f.to.is_some()
|
||||
|| f.subject.is_some()
|
||||
|| f.body.is_some();
|
||||
|
||||
if has_one_filter {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let sort = Some(vec![EmailComparator {
|
||||
property: self.sort.into(),
|
||||
is_ascending: Some(!self.desc),
|
||||
collation: None,
|
||||
keyword: None,
|
||||
}]);
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = QueryJmapEmails::new(
|
||||
jmap.context,
|
||||
filter,
|
||||
sort,
|
||||
Some(self.page.saturating_sub(1) * self.page_size),
|
||||
Some(self.page_size),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let emails = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
QueryJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
QueryJmapEmailsResult::Ok { emails, .. } => break emails,
|
||||
QueryJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
let table = EmailsTable {
|
||||
preset: account.table_preset,
|
||||
arrangement: account.table_arrangement,
|
||||
emails,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_addresses(addrs: &[EmailAddress]) -> String {
|
||||
addrs
|
||||
.iter()
|
||||
.map(|a| {
|
||||
if let Some(name) = &a.name {
|
||||
if !name.is_empty() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
a.email.clone()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct EmailsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
#[serde(skip)]
|
||||
pub arrangement: ContentArrangement,
|
||||
pub emails: Vec<Email>,
|
||||
}
|
||||
|
||||
impl fmt::Display for EmailsTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_content_arrangement(self.arrangement.clone())
|
||||
.set_header(Row::from([
|
||||
Cell::new("ID"),
|
||||
Cell::new("FLAGS"),
|
||||
Cell::new("SUBJECT"),
|
||||
Cell::new("FROM"),
|
||||
Cell::new("DATE"),
|
||||
]));
|
||||
|
||||
for e in &self.emails {
|
||||
let mut flags = String::new();
|
||||
let kw = e.keywords.as_ref();
|
||||
if !kw.and_then(|k| k.get("$seen")).copied().unwrap_or(false) {
|
||||
flags.push('U');
|
||||
}
|
||||
if kw.and_then(|k| k.get("$flagged")).copied().unwrap_or(false) {
|
||||
flags.push('F');
|
||||
}
|
||||
if e.has_attachment.unwrap_or(false) {
|
||||
flags.push('A');
|
||||
}
|
||||
|
||||
let mut row = Row::new();
|
||||
row.max_height(1);
|
||||
row.add_cell(Cell::new(e.id.as_deref().unwrap_or("")));
|
||||
row.add_cell(Cell::new(&flags));
|
||||
row.add_cell(Cell::new(e.subject.as_deref().unwrap_or("")));
|
||||
row.add_cell(Cell::new(format_addresses(
|
||||
e.from.as_deref().unwrap_or(&[]),
|
||||
)));
|
||||
row.add_cell(Cell::new(e.received_at.as_deref().unwrap_or("")));
|
||||
table.add_row(row);
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum SortArg {
|
||||
#[default]
|
||||
ReceivedAt,
|
||||
SentAt,
|
||||
Size,
|
||||
From,
|
||||
To,
|
||||
Subject,
|
||||
HasAttachment,
|
||||
}
|
||||
|
||||
impl fmt::Display for SortArg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::ReceivedAt => write!(f, "received-at"),
|
||||
Self::SentAt => write!(f, "sent-at"),
|
||||
Self::Size => write!(f, "size"),
|
||||
Self::From => write!(f, "from"),
|
||||
Self::To => write!(f, "to"),
|
||||
Self::Subject => write!(f, "subject"),
|
||||
Self::HasAttachment => write!(f, "has-attachment"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortArg> for EmailSortProperty {
|
||||
fn from(arg: SortArg) -> Self {
|
||||
match arg {
|
||||
SortArg::ReceivedAt => EmailSortProperty::ReceivedAt,
|
||||
SortArg::SentAt => EmailSortProperty::SentAt,
|
||||
SortArg::Size => EmailSortProperty::Size,
|
||||
SortArg::From => EmailSortProperty::From,
|
||||
SortArg::To => EmailSortProperty::To,
|
||||
SortArg::Subject => EmailSortProperty::Subject,
|
||||
SortArg::HasAttachment => EmailSortProperty::HasAttachment,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Update JMAP emails via patch operations (Email/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapEmailUpdateCommand {
|
||||
/// Email ID(s) to update.
|
||||
#[arg(value_name = "EMAIL_ID", required = true, num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Add keyword(s) to the email(s).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
pub add_keyword: Vec<String>,
|
||||
|
||||
/// Remove keyword(s) from the email(s).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
pub remove_keyword: Vec<String>,
|
||||
|
||||
/// Replace all keywords atomically (no fetch required).
|
||||
#[arg(long, value_name = "KEYWORD", num_args = 0..)]
|
||||
pub keywords: Option<Vec<String>>,
|
||||
|
||||
/// Add email(s) to a mailbox.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 1..)]
|
||||
pub add_mailbox: Vec<String>,
|
||||
|
||||
/// Remove email(s) from a mailbox.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 1..)]
|
||||
pub remove_mailbox: Vec<String>,
|
||||
|
||||
/// Replace all mailbox memberships atomically.
|
||||
#[arg(long, value_name = "MAILBOX-ID", num_args = 0..)]
|
||||
pub mailboxes: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl JmapEmailUpdateCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in &self.ids {
|
||||
for kw in &self.add_keyword {
|
||||
args.set_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
|
||||
for kw in &self.remove_keyword {
|
||||
args.unset_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
|
||||
if let Some(kws) = &self.keywords {
|
||||
let map: HashMap<String, bool> = kws.iter().map(|kw| (kw.clone(), true)).collect();
|
||||
args.replace_keywords(id.clone(), map);
|
||||
}
|
||||
|
||||
for mbox in &self.add_mailbox {
|
||||
args.add_to_mailbox(id.clone(), mbox.clone());
|
||||
}
|
||||
|
||||
for mbox in &self.remove_mailbox {
|
||||
args.remove_from_mailbox(id.clone(), mbox.clone());
|
||||
}
|
||||
|
||||
if let Some(mboxes) = &self.mailboxes {
|
||||
let map: HashMap<String, bool> = mboxes.iter().map(|m| (m.clone(), true)).collect();
|
||||
args.replace_mailbox_ids(id.clone(), map);
|
||||
}
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("Failed to update email `{id}`");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Email(s) successfully updated"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
identity::{
|
||||
create::CreateIdentityCommand, delete::DeleteIdentityCommand, get::GetIdentityCommand,
|
||||
update::UpdateIdentityCommand,
|
||||
},
|
||||
};
|
||||
|
||||
/// Manage JMAP sender identities.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum IdentityCommand {
|
||||
/// Fetch identities (Identity/get).
|
||||
#[command(aliases = ["lst", "list"])]
|
||||
Get(GetIdentityCommand),
|
||||
/// Create a new identity (Identity/set).
|
||||
#[command(aliases = ["add", "new"])]
|
||||
Create(CreateIdentityCommand),
|
||||
/// Update an existing identity (Identity/set).
|
||||
#[command(alias = "edit")]
|
||||
Update(UpdateIdentityCommand),
|
||||
/// Delete an identity (Identity/set).
|
||||
#[command(aliases = ["remove", "rm"])]
|
||||
Delete(DeleteIdentityCommand),
|
||||
}
|
||||
|
||||
impl IdentityCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Create(cmd) => cmd.execute(printer, account),
|
||||
Self::Update(cmd) => cmd.execute(printer, account),
|
||||
Self::Delete(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::identity_set::{IdentitySetArgs, SetJmapIdentities, SetJmapIdentitiesResult},
|
||||
types::identity::IdentityCreate,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Create a JMAP sender identity (Identity/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CreateIdentityCommand {
|
||||
/// Display name for the sender.
|
||||
pub name: String,
|
||||
|
||||
/// Email address for the sender.
|
||||
pub email: String,
|
||||
|
||||
/// Plaintext signature to append to outgoing emails.
|
||||
#[arg(long)]
|
||||
pub text_signature: Option<String>,
|
||||
|
||||
/// HTML signature to append to outgoing emails.
|
||||
#[arg(long)]
|
||||
pub html_signature: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateIdentityCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let identity = IdentityCreate {
|
||||
name: self.name.clone(),
|
||||
email: self.email.clone(),
|
||||
reply_to: None,
|
||||
bcc: None,
|
||||
text_signature: self.text_signature,
|
||||
html_signature: self.html_signature,
|
||||
};
|
||||
|
||||
let mut args = IdentitySetArgs::default();
|
||||
args.create(self.email.clone(), identity);
|
||||
|
||||
let mut coroutine = SetJmapIdentities::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapIdentitiesResult::Ok { not_created, .. } => break not_created,
|
||||
SetJmapIdentitiesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.email) {
|
||||
let mut ctx = anyhow!("Failed to create identity `{}`", self.email);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Identity successfully created"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::identity_set::{
|
||||
IdentitySetArgs, SetJmapIdentities, SetJmapIdentitiesResult,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Delete a JMAP sender identity (Identity/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DeleteIdentityCommand {
|
||||
/// Identity ID(s) to delete.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl DeleteIdentityCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = IdentitySetArgs::default();
|
||||
|
||||
for id in self.ids {
|
||||
args.destroy(id);
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapIdentities::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_destroyed = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapIdentitiesResult::Ok { not_destroyed, .. } => break not_destroyed,
|
||||
SetJmapIdentitiesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_destroyed {
|
||||
let mut ctx = anyhow!("Failed to delete identity `{id}`");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Identity successfully deleted"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::identity_get::{GetJmapIdentities, GetJmapIdentitiesResult},
|
||||
types::identity::Identity,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Get JMAP identities (Identity/get).
|
||||
///
|
||||
/// Lists sender identities available for sending email. Pass no IDs to
|
||||
/// list all identities.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetIdentityCommand {
|
||||
/// Identity ID(s) to retrieve (omit to get all).
|
||||
#[arg(value_name = "ID")]
|
||||
pub ids: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl GetIdentityCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine = GetJmapIdentities::new(jmap.context, self.ids)?;
|
||||
let mut arg = None;
|
||||
|
||||
let (identities, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapIdentitiesResult::Ok {
|
||||
identities,
|
||||
not_found,
|
||||
..
|
||||
} => break (identities, not_found),
|
||||
GetJmapIdentitiesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
warn!("identity `{id}` not found");
|
||||
}
|
||||
|
||||
let table = IdentitiesTable {
|
||||
preset: account.table_preset,
|
||||
identities,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct IdentitiesTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub identities: Vec<Identity>,
|
||||
}
|
||||
|
||||
impl fmt::Display for IdentitiesTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_header(Row::from([
|
||||
Cell::new("ID"),
|
||||
Cell::new("NAME"),
|
||||
Cell::new("EMAIL"),
|
||||
]))
|
||||
.add_rows(
|
||||
self.identities.iter().map(|i| {
|
||||
Row::from([Cell::new(&i.id), Cell::new(&i.name), Cell::new(&i.email)])
|
||||
}),
|
||||
);
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod command;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod get;
|
||||
pub mod update;
|
||||
@@ -0,0 +1,69 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::identity_set::{IdentitySetArgs, SetJmapIdentities, SetJmapIdentitiesResult},
|
||||
types::identity::IdentityUpdate,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Update a JMAP sender identity (Identity/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct UpdateIdentityCommand {
|
||||
/// Identity ID to update.
|
||||
pub id: String,
|
||||
|
||||
/// New display name.
|
||||
#[arg(long)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// New plaintext signature.
|
||||
#[arg(long)]
|
||||
pub text_signature: Option<String>,
|
||||
|
||||
/// New HTML signature.
|
||||
#[arg(long)]
|
||||
pub html_signature: Option<String>,
|
||||
}
|
||||
|
||||
impl UpdateIdentityCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let patch = IdentityUpdate {
|
||||
name: self.name,
|
||||
reply_to: None,
|
||||
bcc: None,
|
||||
text_signature: self.text_signature,
|
||||
html_signature: self.html_signature,
|
||||
};
|
||||
|
||||
let mut args = IdentitySetArgs::default();
|
||||
args.update(self.id.clone(), patch);
|
||||
|
||||
let mut coroutine = SetJmapIdentities::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapIdentitiesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapIdentitiesResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapIdentitiesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_updated.get(&self.id) {
|
||||
let mut ctx = anyhow!("Failed to update identity `{}`", self.id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Identity successfully updated"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Add keywords to JMAP emails.
|
||||
///
|
||||
/// Standard JMAP keywords: `$seen`, `$flagged`, `$answered`, `$draft`.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddKeywordCommand {
|
||||
/// Email ID(s) to add the keyword(s) to.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// The keyword(s) to add.
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl AddKeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in &self.ids {
|
||||
for kw in &self.keyword {
|
||||
args.set_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to add keyword to email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"Keyword(s) `{}` added to {} email(s).",
|
||||
self.keyword.join(", "),
|
||||
self.ids.len()
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
keyword::{add::AddKeywordCommand, remove::RemoveKeywordCommand, set::SetKeywordsCommand},
|
||||
};
|
||||
|
||||
/// Manage JMAP email keywords (flags).
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum KeywordCommand {
|
||||
Add(AddKeywordCommand),
|
||||
Remove(RemoveKeywordCommand),
|
||||
Set(SetKeywordsCommand),
|
||||
}
|
||||
|
||||
impl KeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Add(cmd) => cmd.execute(printer, account),
|
||||
Self::Remove(cmd) => cmd.execute(printer, account),
|
||||
Self::Set(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
pub mod add;
|
||||
pub mod command;
|
||||
pub mod remove;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,61 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Remove keywords from JMAP emails.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct RemoveKeywordCommand {
|
||||
/// Email ID(s) to remove the keyword(s) from.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// The keyword(s) to remove.
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl RemoveKeywordCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
|
||||
for id in &self.ids {
|
||||
for kw in &self.keyword {
|
||||
args.unset_keyword(id.clone(), kw.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to remove keyword from email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!(
|
||||
"Keyword(s) `{}` removed from {} email(s).",
|
||||
self.keyword.join(", "),
|
||||
self.ids.len()
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_set::{EmailSetArgs, SetJmapEmails, SetJmapEmailsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Replace all keywords on JMAP emails.
|
||||
///
|
||||
/// Replaces the entire set of keywords atomically — no need to know
|
||||
/// the current keywords first.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetKeywordsCommand {
|
||||
/// Email ID(s) to update.
|
||||
#[arg(value_name = "EMAIL_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Keywords to set (replaces all existing keywords).
|
||||
#[arg(long, short, num_args = 1..)]
|
||||
pub keyword: Vec<String>,
|
||||
}
|
||||
|
||||
impl SetKeywordsCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let keywords: HashMap<String, bool> =
|
||||
self.keyword.iter().map(|kw| (kw.clone(), true)).collect();
|
||||
|
||||
let mut args = EmailSetArgs::default();
|
||||
for id in &self.ids {
|
||||
args.replace_keywords(id.clone(), keywords.clone());
|
||||
}
|
||||
|
||||
let mut coroutine = SetJmapEmails::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapEmailsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapEmailsResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapEmailsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for (id, err) in ¬_updated {
|
||||
let mut ctx = anyhow!("failed to set keywords on email `{id}`");
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.log(format!("Keywords set on {} email(s).", self.ids.len()))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
mailbox::{
|
||||
create::JmapMailboxCreateCommand, destroy::JmapMailboxDestroyCommand,
|
||||
get::JmapMailboxGetCommand, query::JmapMailboxQueryCommand,
|
||||
update::JmapMailboxUpdateCommand,
|
||||
},
|
||||
};
|
||||
|
||||
/// Manage JMAP mailboxes.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum JmapMailboxCommand {
|
||||
Get(JmapMailboxGetCommand),
|
||||
Query(JmapMailboxQueryCommand),
|
||||
#[command(visible_aliases = ["add", "new"])]
|
||||
Create(JmapMailboxCreateCommand),
|
||||
Update(JmapMailboxUpdateCommand),
|
||||
#[command(visible_aliases = ["delete", "del", "remove", "rm"])]
|
||||
Destroy(JmapMailboxDestroyCommand),
|
||||
}
|
||||
|
||||
impl JmapMailboxCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Query(cmd) => cmd.execute(printer, account),
|
||||
Self::Create(cmd) => cmd.execute(printer, account),
|
||||
Self::Update(cmd) => cmd.execute(printer, account),
|
||||
Self::Destroy(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::mailbox_set::{MailboxSetArgs, SetJmapMailboxes, SetJmapMailboxesResult},
|
||||
types::mailbox::MailboxCreate,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Create a JMAP mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxCreateCommand {
|
||||
/// The name of the new mailbox.
|
||||
#[arg(value_name = "NAME")]
|
||||
pub name: String,
|
||||
|
||||
/// Attach the new mailbox to the parent mailbox matching the
|
||||
/// given identifier.
|
||||
#[arg(long, value_name = "ID")]
|
||||
pub parent_id: Option<String>,
|
||||
|
||||
/// Should subscribe to the new mailbox.
|
||||
#[arg(long, value_name = "NAME")]
|
||||
pub subscribe: bool,
|
||||
}
|
||||
|
||||
impl JmapMailboxCreateCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let new_mailbox = MailboxCreate {
|
||||
name: Some(self.name.clone()),
|
||||
parent_id: self.parent_id,
|
||||
is_subscribed: if self.subscribe { Some(true) } else { None },
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut create = HashMap::new();
|
||||
create.insert(self.name.clone(), new_mailbox);
|
||||
|
||||
let mut args = MailboxSetArgs::default();
|
||||
args.create = Some(create);
|
||||
|
||||
let mut coroutine = SetJmapMailboxes::new(jmap.context, args)?;
|
||||
let mut arg = None;
|
||||
|
||||
let not_created = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapMailboxesResult::Ok { not_created, .. } => break not_created,
|
||||
SetJmapMailboxesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_created.get(&self.name) {
|
||||
let mut ctx = anyhow!("Create JMAP mailbox `{}` error", self.name);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Mailbox successfully created"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::mailbox_set::{MailboxSetArgs, SetJmapMailboxes, SetJmapMailboxesResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Delete a JMAP mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxDestroyCommand {
|
||||
/// The ID of the mailbox to delete.
|
||||
#[arg(value_name = "ID", required = true, num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
|
||||
/// Destroy all emails in the mailbox when deleting.
|
||||
#[arg(long, default_value_t)]
|
||||
pub purge: bool,
|
||||
}
|
||||
|
||||
impl JmapMailboxDestroyCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut args = MailboxSetArgs::default();
|
||||
args.destroy = Some(self.ids.clone());
|
||||
args.on_destroy_remove_emails = if self.purge { Some(true) } else { None };
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = SetJmapMailboxes::new(jmap.context, args)?;
|
||||
|
||||
let not_destroyed = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapMailboxesResult::Ok { not_destroyed, .. } => break not_destroyed,
|
||||
SetJmapMailboxesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for ref id in self.ids {
|
||||
if let Some(err) = not_destroyed.get(id) {
|
||||
let mut ctx = anyhow!("Update JMAP mailbox `{id}` error");
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new("Mailbox successfully deleted"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::mailbox_get::{GetJmapMailboxes, GetJmapMailboxesResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use log::warn;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{account::JmapAccount, mailbox::query::MailboxesTable};
|
||||
|
||||
/// Get JMAP mailboxes by ID (Mailbox/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxGetCommand {
|
||||
/// Mailbox ID(s) to retrieve.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl JmapMailboxGetCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine = GetJmapMailboxes::new(jmap.context, Some(self.ids.clone()), None)?;
|
||||
let mut arg = None;
|
||||
|
||||
let (mailboxes, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapMailboxesResult::Ok {
|
||||
mailboxes,
|
||||
not_found,
|
||||
..
|
||||
} => break (mailboxes, not_found),
|
||||
GetJmapMailboxesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in not_found {
|
||||
warn!("mailbox `{id}` not found");
|
||||
}
|
||||
|
||||
let table = MailboxesTable {
|
||||
preset: account.table_preset,
|
||||
mailboxes,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod command;
|
||||
pub mod create;
|
||||
pub mod destroy;
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
pub mod update;
|
||||
@@ -0,0 +1,238 @@
|
||||
use std::{convert::Infallible, fmt, str::FromStr};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, ValueEnum};
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::mailbox_query::{QueryJmapMailboxes, QueryJmapMailboxesResult},
|
||||
types::mailbox::{
|
||||
Mailbox, MailboxFilter, MailboxRole, MailboxSortComparator, MailboxSortProperty,
|
||||
},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Query JMAP mailboxes (Mailbox/query + Mailbox/get).
|
||||
///
|
||||
/// Lists, filters and sorts mailboxes.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxQueryCommand {
|
||||
/// Filter by parent mailbox identifier.
|
||||
#[arg(long, value_name = "ID")]
|
||||
pub parent_id: Option<String>,
|
||||
|
||||
/// Filter by role [possible values: inbox, archive, drafts,
|
||||
/// flagged, important, junk, sent, subscribed, trash].
|
||||
#[arg(long, value_name = "ROLE")]
|
||||
pub role: Option<RoleArg>,
|
||||
|
||||
/// Filter by substring name match.
|
||||
#[arg(long, value_name = "NAME")]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// List all mailboxes, not just subscribed ones.
|
||||
#[arg(long, default_value_t)]
|
||||
pub all: bool,
|
||||
|
||||
/// Only return mailboxes that have a role.
|
||||
#[arg(long, default_value_t)]
|
||||
pub has_any_role: bool,
|
||||
|
||||
/// Sort by property.
|
||||
#[arg(long, value_name = "PROP", default_value_t)]
|
||||
pub sort: SortArg,
|
||||
|
||||
/// Sort in descending order.
|
||||
#[arg(long, default_value_t)]
|
||||
pub desc: bool,
|
||||
|
||||
/// Number of mailboxes to display per page.
|
||||
#[arg(long, short = 's', value_name = "N", default_value = "10")]
|
||||
pub page_size: u64,
|
||||
|
||||
/// Page index, starting from 1.
|
||||
#[arg(long, short, value_name = "N", default_value = "1")]
|
||||
pub page: u64,
|
||||
}
|
||||
|
||||
impl JmapMailboxQueryCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let filter = {
|
||||
let f = MailboxFilter {
|
||||
parent_id: self.parent_id,
|
||||
role: self.role.map(Into::into),
|
||||
name: self.name,
|
||||
is_subscribed: if self.all { None } else { Some(true) },
|
||||
has_any_role: if self.has_any_role { Some(true) } else { None },
|
||||
};
|
||||
|
||||
let has_one_filter = f.parent_id.is_some()
|
||||
|| f.role.is_some()
|
||||
|| f.name.is_some()
|
||||
|| f.is_subscribed.is_some()
|
||||
|| f.has_any_role.is_some();
|
||||
|
||||
if has_one_filter {
|
||||
Some(f)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let sort = Some(vec![MailboxSortComparator {
|
||||
property: self.sort.into(),
|
||||
is_ascending: Some(!self.desc),
|
||||
}]);
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = QueryJmapMailboxes::new(
|
||||
jmap.context,
|
||||
filter,
|
||||
sort,
|
||||
Some(self.page.saturating_sub(1) * self.page_size),
|
||||
Some(self.page_size),
|
||||
None,
|
||||
)?;
|
||||
|
||||
let mailboxes = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
QueryJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
QueryJmapMailboxesResult::Ok { mailboxes, .. } => break mailboxes,
|
||||
QueryJmapMailboxesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
let table = MailboxesTable {
|
||||
preset: account.table_preset,
|
||||
mailboxes,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct MailboxesTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub mailboxes: Vec<Mailbox>,
|
||||
}
|
||||
|
||||
impl fmt::Display for MailboxesTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_header(Row::from([
|
||||
Cell::new("ID"),
|
||||
Cell::new("NAME"),
|
||||
Cell::new("ROLE"),
|
||||
Cell::new("TOTAL"),
|
||||
Cell::new("UNREAD"),
|
||||
Cell::new("SUBSCRIBED"),
|
||||
]))
|
||||
.add_rows(self.mailboxes.iter().map(|r| {
|
||||
let mut row = Row::new();
|
||||
row.max_height(1)
|
||||
.add_cell(Cell::new(r.id.as_deref().unwrap_or("Unknown")))
|
||||
.add_cell(Cell::new(r.name.as_deref().unwrap_or("Unknown")))
|
||||
.add_cell(match r.role.as_ref() {
|
||||
Some(r) => Cell::new(r.to_string()),
|
||||
None => Cell::new(""),
|
||||
})
|
||||
.add_cell(Cell::new(r.total_emails))
|
||||
.add_cell(Cell::new(r.unread_emails))
|
||||
.add_cell(Cell::new(if r.is_subscribed { "yes" } else { "" }));
|
||||
row
|
||||
}));
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum RoleArg {
|
||||
Inbox,
|
||||
Archive,
|
||||
Drafts,
|
||||
Flagged,
|
||||
Important,
|
||||
Junk,
|
||||
Sent,
|
||||
Subscribed,
|
||||
Trash,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl FromStr for RoleArg {
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"inbox" => Ok(Self::Inbox),
|
||||
"archive" => Ok(Self::Archive),
|
||||
"drafts" => Ok(Self::Drafts),
|
||||
"flagged" => Ok(Self::Flagged),
|
||||
"important" => Ok(Self::Important),
|
||||
"junk" => Ok(Self::Junk),
|
||||
"sent" => Ok(Self::Sent),
|
||||
"subscribed" => Ok(Self::Subscribed),
|
||||
"trash" => Ok(Self::Trash),
|
||||
other => Ok(Self::Other(other.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RoleArg> for MailboxRole {
|
||||
fn from(arg: RoleArg) -> Self {
|
||||
match arg {
|
||||
RoleArg::Inbox => MailboxRole::Inbox,
|
||||
RoleArg::Archive => MailboxRole::Archive,
|
||||
RoleArg::Drafts => MailboxRole::Drafts,
|
||||
RoleArg::Flagged => MailboxRole::Flagged,
|
||||
RoleArg::Important => MailboxRole::Important,
|
||||
RoleArg::Junk => MailboxRole::Junk,
|
||||
RoleArg::Sent => MailboxRole::Sent,
|
||||
RoleArg::Subscribed => MailboxRole::Subscribed,
|
||||
RoleArg::Trash => MailboxRole::Trash,
|
||||
RoleArg::Other(s) => MailboxRole::Other(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, ValueEnum)]
|
||||
#[clap(rename_all = "kebab-case")]
|
||||
pub enum SortArg {
|
||||
Name,
|
||||
#[default]
|
||||
SortOrder,
|
||||
ParentId,
|
||||
}
|
||||
|
||||
impl fmt::Display for SortArg {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Name => write!(f, "name"),
|
||||
Self::SortOrder => write!(f, "sort-order"),
|
||||
Self::ParentId => write!(f, "parent-id"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SortArg> for MailboxSortProperty {
|
||||
fn from(arg: SortArg) -> Self {
|
||||
match arg {
|
||||
SortArg::Name => MailboxSortProperty::Name,
|
||||
SortArg::SortOrder => MailboxSortProperty::SortOrder,
|
||||
SortArg::ParentId => MailboxSortProperty::ParentId,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::mailbox_set::{MailboxSetArgs, SetJmapMailboxes, SetJmapMailboxesResult},
|
||||
types::mailbox::MailboxUpdate,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::{account::JmapAccount, mailbox::query::RoleArg};
|
||||
|
||||
/// Update a JMAP mailbox.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct JmapMailboxUpdateCommand {
|
||||
/// The ID of the mailbox to update.
|
||||
#[arg(value_name = "ID")]
|
||||
pub id: String,
|
||||
|
||||
/// New display name.
|
||||
#[arg(long)]
|
||||
pub name: Option<String>,
|
||||
|
||||
/// New parent mailbox ID.
|
||||
#[arg(long, value_name = "ID")]
|
||||
pub parent_id: Option<String>,
|
||||
|
||||
/// New role.
|
||||
#[arg(long, value_name = "ROLE")]
|
||||
pub role: Option<RoleArg>,
|
||||
|
||||
/// New sort order.
|
||||
#[arg(long, value_name = "N")]
|
||||
pub sort_order: Option<u32>,
|
||||
|
||||
/// Subscribe to the mailbox.
|
||||
#[arg(long, conflicts_with = "unsubscribe")]
|
||||
pub subscribe: bool,
|
||||
|
||||
/// Unsubscribe from the mailbox.
|
||||
#[arg(long, conflicts_with = "subscribe")]
|
||||
pub unsubscribe: bool,
|
||||
}
|
||||
|
||||
impl JmapMailboxUpdateCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let is_subscribed = if self.subscribe {
|
||||
Some(true)
|
||||
} else if self.unsubscribe {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let patch = MailboxUpdate {
|
||||
name: self.name,
|
||||
parent_id: self.parent_id,
|
||||
role: self.role.map(Into::into),
|
||||
sort_order: self.sort_order,
|
||||
is_subscribed,
|
||||
};
|
||||
|
||||
let mut update = HashMap::new();
|
||||
update.insert(self.id.clone(), patch);
|
||||
|
||||
let mut args = MailboxSetArgs::default();
|
||||
args.update = Some(update);
|
||||
|
||||
let mut arg = None;
|
||||
let mut coroutine = SetJmapMailboxes::new(jmap.context, args)?;
|
||||
|
||||
let not_updated = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapMailboxesResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SetJmapMailboxesResult::Ok { not_updated, .. } => break not_updated,
|
||||
SetJmapMailboxesResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(err) = not_updated.get(&self.id) {
|
||||
let mut ctx = anyhow!("Update JMAP mailbox `{}` error", self.id);
|
||||
|
||||
if let Some(desc) = &err.description {
|
||||
ctx = anyhow!(desc.clone()).context(ctx);
|
||||
}
|
||||
|
||||
if !err.properties.is_empty() {
|
||||
ctx = anyhow!("Invalid properties: {}", err.properties.join(", ")).context(ctx);
|
||||
}
|
||||
|
||||
bail!(ctx);
|
||||
}
|
||||
|
||||
printer.out(Message::new("Mailbox successfully updated"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod account;
|
||||
pub mod command;
|
||||
pub mod email;
|
||||
pub mod identity;
|
||||
pub mod mailbox;
|
||||
pub mod query;
|
||||
pub mod submission;
|
||||
pub mod thread;
|
||||
pub mod vacation;
|
||||
@@ -0,0 +1,139 @@
|
||||
use std::{
|
||||
fmt,
|
||||
io::{stdin, BufRead},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::send::{JmapRequest, SendJmapRequest, SendJmapRequestResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Send a raw JMAP method-calls array and print the response.
|
||||
///
|
||||
/// METHOD_CALLS must be a JSON array of JMAP method call tuples:
|
||||
///
|
||||
/// '[["Mailbox/query", {"filter": {"role": "inbox"}}, "c0"]]'
|
||||
///
|
||||
/// The `accountId` field is injected into each call's arguments
|
||||
/// automatically if not already present. Pass `-` or omit to read
|
||||
/// from stdin.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct QueryCommand {
|
||||
/// Extra capability URNs to declare (core and mail are always included).
|
||||
#[arg(long = "using", value_name = "URN")]
|
||||
pub using: Vec<String>,
|
||||
|
||||
/// The JMAP methodCalls JSON array (or omit / pass `-` to read stdin).
|
||||
#[arg(trailing_var_arg = true)]
|
||||
#[arg(name = "method-calls", value_name = "METHOD_CALLS")]
|
||||
pub method_calls: Vec<String>,
|
||||
}
|
||||
|
||||
impl QueryCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let raw = if self.method_calls.is_empty()
|
||||
|| self.method_calls.first().map(|s| s.as_str()) == Some("-")
|
||||
{
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.map_while(Result::ok)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
} else {
|
||||
self.method_calls.join(" ")
|
||||
};
|
||||
|
||||
let calls_value: serde_json::Value =
|
||||
serde_json::from_str(&raw).context("METHOD_CALLS is not valid JSON")?;
|
||||
|
||||
let serde_json::Value::Array(calls_arr) = calls_value else {
|
||||
bail!("METHOD_CALLS must be a JSON array");
|
||||
};
|
||||
|
||||
let account_id = jmap.context.account_id.clone().unwrap_or_default();
|
||||
|
||||
// Parse and inject accountId into each call's args.
|
||||
let mut method_calls = Vec::with_capacity(calls_arr.len());
|
||||
for (i, call) in calls_arr.into_iter().enumerate() {
|
||||
let serde_json::Value::Array(mut tuple) = call else {
|
||||
bail!("method call #{i} must be a JSON array [name, args, callId]");
|
||||
};
|
||||
if tuple.len() != 3 {
|
||||
bail!("method call #{i} must have exactly 3 elements [name, args, callId]");
|
||||
}
|
||||
let call_id = match tuple.remove(2) {
|
||||
serde_json::Value::String(s) => s,
|
||||
v => bail!("method call #{i} callId must be a string, got {v}"),
|
||||
};
|
||||
let mut args = tuple.remove(1);
|
||||
let name = match tuple.remove(0) {
|
||||
serde_json::Value::String(s) => s,
|
||||
v => bail!("method call #{i} name must be a string, got {v}"),
|
||||
};
|
||||
// Inject accountId if the args object doesn't already have it.
|
||||
if let serde_json::Value::Object(ref mut map) = args {
|
||||
map.entry("accountId")
|
||||
.or_insert_with(|| serde_json::Value::String(account_id.clone()));
|
||||
}
|
||||
method_calls.push((name, args, call_id));
|
||||
}
|
||||
|
||||
let mut using = vec![
|
||||
io_jmap::types::session::capabilities::CORE.to_string(),
|
||||
io_jmap::types::session::capabilities::MAIL.to_string(),
|
||||
];
|
||||
for extra in self.using {
|
||||
if !using.contains(&extra) {
|
||||
using.push(extra);
|
||||
}
|
||||
}
|
||||
|
||||
let api_url = jmap
|
||||
.context
|
||||
.api_url()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "http://localhost".parse().unwrap());
|
||||
|
||||
let request = JmapRequest {
|
||||
using,
|
||||
method_calls,
|
||||
created_ids: None,
|
||||
};
|
||||
|
||||
let mut coroutine = SendJmapRequest::new(jmap.context, &api_url, request)?;
|
||||
let mut arg = None;
|
||||
|
||||
let response = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SendJmapRequestResult::Ok { context, response, .. } => {
|
||||
jmap.context = context;
|
||||
break response;
|
||||
}
|
||||
SendJmapRequestResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SendJmapRequestResult::Err { err, .. } => return Err(err.into()),
|
||||
}
|
||||
};
|
||||
|
||||
printer.out(RawResponse(response.method_responses))
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps the raw method_responses for display.
|
||||
#[derive(Serialize)]
|
||||
struct RawResponse(Vec<(String, serde_json::Value, String)>);
|
||||
|
||||
impl fmt::Display for RawResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match serde_json::to_string_pretty(&self.0) {
|
||||
Ok(s) => write!(f, "{s}"),
|
||||
Err(e) => write!(f, "<serialization error: {e}>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use clap::Parser;
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Cancel (undo) a pending JMAP email submission (EmailSubmission/set).
|
||||
///
|
||||
/// Only submissions with `undoStatus: "pending"` can be canceled.
|
||||
/// The server may reject this if the message has already been sent.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CancelSubmissionCommand {
|
||||
/// Submission ID(s) to cancel.
|
||||
#[arg(value_name = "SUBMISSION_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl CancelSubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
// EmailSubmission/set update: set undoStatus to "canceled"
|
||||
let update: std::collections::HashMap<String, serde_json::Value> = self
|
||||
.ids
|
||||
.iter()
|
||||
.map(|id| {
|
||||
(
|
||||
id.clone(),
|
||||
serde_json::json!({ "undoStatus": "canceled" }),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let args = serde_json::json!({
|
||||
"update": update
|
||||
});
|
||||
|
||||
// Use the raw query approach via SubmitJmapEmail isn't suitable here;
|
||||
// we need a direct EmailSubmission/set update. Use the query command
|
||||
// pattern with a raw request instead.
|
||||
//
|
||||
// For now, build the request directly via the send coroutine.
|
||||
use io_jmap::{
|
||||
coroutines::send::{JmapBatch, SendJmapRequest, SendJmapRequestResult},
|
||||
types::session::capabilities,
|
||||
};
|
||||
|
||||
let account_id = jmap.context.account_id.clone().unwrap_or_default();
|
||||
let api_url = jmap
|
||||
.context
|
||||
.api_url()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "http://localhost".parse().unwrap());
|
||||
|
||||
let mut json_args = args.clone();
|
||||
json_args["accountId"] = serde_json::json!(account_id);
|
||||
|
||||
let mut batch = JmapBatch::new();
|
||||
batch.add("EmailSubmission/set", json_args);
|
||||
let request = batch.into_request(vec![
|
||||
capabilities::CORE.into(),
|
||||
capabilities::MAIL.into(),
|
||||
capabilities::SUBMISSION.into(),
|
||||
]);
|
||||
|
||||
let mut send = SendJmapRequest::new(jmap.context, &api_url, request)
|
||||
.map_err(|e| anyhow!("{e}"))?;
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match send.resume(arg.take()) {
|
||||
SendJmapRequestResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SendJmapRequestResult::Ok { context, response, .. } => {
|
||||
jmap.context = context;
|
||||
if let Some((name, args, _)) =
|
||||
response.method_responses.into_iter().next()
|
||||
{
|
||||
if name == "error" {
|
||||
bail!("EmailSubmission/set error: {args}");
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
SendJmapRequestResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new(format!(
|
||||
"{} submission(s) canceled.",
|
||||
self.ids.len()
|
||||
)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
submission::{
|
||||
cancel::CancelSubmissionCommand, create::CreateSubmissionCommand,
|
||||
get::GetSubmissionCommand, query::QuerySubmissionCommand,
|
||||
},
|
||||
};
|
||||
|
||||
/// Manage JMAP email submissions.
|
||||
#[derive(Debug, Subcommand)]
|
||||
#[command(rename_all = "kebab-case")]
|
||||
pub enum SubmissionCommand {
|
||||
/// Fetch submissions by ID (EmailSubmission/get).
|
||||
Get(GetSubmissionCommand),
|
||||
/// Query and list submissions (EmailSubmission/query + EmailSubmission/get).
|
||||
#[command(aliases = ["lst", "list"])]
|
||||
Query(QuerySubmissionCommand),
|
||||
/// Submit a draft email for sending (EmailSubmission/set).
|
||||
#[command(aliases = ["send", "submit"])]
|
||||
Create(CreateSubmissionCommand),
|
||||
/// Cancel a pending submission (EmailSubmission/set).
|
||||
Cancel(CancelSubmissionCommand),
|
||||
}
|
||||
|
||||
impl SubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Query(cmd) => cmd.execute(printer, account),
|
||||
Self::Create(cmd) => cmd.execute(printer, account),
|
||||
Self::Cancel(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::email_submission_set::{SubmitJmapEmail, SubmitJmapEmailResult},
|
||||
types::email_submission::{EmailAddressWithParameters, EmailSubmissionCreate, Envelope},
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Submit a JMAP email for sending (EmailSubmission/set).
|
||||
///
|
||||
/// The email must already exist as a draft in the JMAP account.
|
||||
/// This is the JMAP equivalent of SMTP message submission.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct CreateSubmissionCommand {
|
||||
/// The ID of the draft email to send.
|
||||
#[arg(value_name = "EMAIL_ID")]
|
||||
pub email_id: String,
|
||||
|
||||
/// The identity ID to send as (from `identity get`).
|
||||
#[arg(long, value_name = "IDENTITY_ID")]
|
||||
pub identity_id: String,
|
||||
|
||||
/// Override the MAIL FROM address (uses `From` header if omitted).
|
||||
#[arg(long, value_name = "ADDRESS")]
|
||||
pub mail_from: Option<String>,
|
||||
|
||||
/// Override the RCPT TO addresses (uses `To`, `Cc`, `Bcc` if omitted).
|
||||
#[arg(long, value_name = "ADDRESS")]
|
||||
pub rcpt_to: Vec<String>,
|
||||
}
|
||||
|
||||
impl CreateSubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let envelope = if let Some(mail_from_addr) = self.mail_from {
|
||||
let rcpt_to = self
|
||||
.rcpt_to
|
||||
.into_iter()
|
||||
.map(|addr| EmailAddressWithParameters { email: addr, parameters: None })
|
||||
.collect();
|
||||
Some(Envelope {
|
||||
mail_from: EmailAddressWithParameters {
|
||||
email: mail_from_addr,
|
||||
parameters: None,
|
||||
},
|
||||
rcpt_to,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let submission = EmailSubmissionCreate {
|
||||
identity_id: self.identity_id,
|
||||
email_id: self.email_id.clone(),
|
||||
envelope,
|
||||
};
|
||||
|
||||
let mut submissions = std::collections::HashMap::new();
|
||||
submissions.insert("send-1".to_string(), submission);
|
||||
|
||||
let mut coroutine = SubmitJmapEmail::new(jmap.context, submissions)?;
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SubmitJmapEmailResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
SubmitJmapEmailResult::Ok { context, not_created, .. } => {
|
||||
jmap.context = context;
|
||||
|
||||
if let Some(err) = not_created.get("send-1") {
|
||||
bail!(
|
||||
"failed to send email `{}`: {} — {}",
|
||||
self.email_id,
|
||||
err.error_type,
|
||||
err.description.as_deref().unwrap_or("no description")
|
||||
);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
SubmitJmapEmailResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
|
||||
printer.log(format!("Email `{}` successfully sent.", self.email_id))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::email_submission_get::{
|
||||
GetJmapEmailSubmissions, GetJmapEmailSubmissionsResult,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Get JMAP email submissions by ID (EmailSubmission/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetSubmissionCommand {
|
||||
/// Submission ID(s) to retrieve.
|
||||
#[arg(value_name = "SUBMISSION_ID", num_args = 1..)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl GetSubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine =
|
||||
GetJmapEmailSubmissions::new(jmap.context, Some(self.ids.clone()))?;
|
||||
let mut arg = None;
|
||||
|
||||
let (submissions, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapEmailSubmissionsResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
GetJmapEmailSubmissionsResult::Ok {
|
||||
context,
|
||||
submissions,
|
||||
not_found,
|
||||
..
|
||||
} => {
|
||||
jmap.context = context;
|
||||
break (submissions, not_found);
|
||||
}
|
||||
GetJmapEmailSubmissionsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Submission `{id}` not found."))?;
|
||||
}
|
||||
|
||||
printer.out(serde_json::to_value(&submissions)?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod cancel;
|
||||
pub mod command;
|
||||
pub mod create;
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
@@ -0,0 +1,123 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use comfy_table::{Cell, Row, Table};
|
||||
use io_jmap::{
|
||||
coroutines::email_submission_query::{
|
||||
QueryJmapEmailSubmissions, QueryJmapEmailSubmissionsResult,
|
||||
},
|
||||
types::email_submission::EmailSubmission,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Query JMAP email submissions (EmailSubmission/query + EmailSubmission/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct QuerySubmissionCommand {
|
||||
/// Filter by undo status (`pending`, `final`, `canceled`).
|
||||
#[arg(long, value_name = "STATUS")]
|
||||
pub undo_status: Option<String>,
|
||||
|
||||
/// Filter by sent-before date (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub before: Option<String>,
|
||||
|
||||
/// Filter by sent-after date (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub after: Option<String>,
|
||||
|
||||
/// Number of submissions to display per page.
|
||||
#[arg(long, short = 's', value_name = "N", default_value = "10")]
|
||||
pub page_size: u64,
|
||||
|
||||
/// Page index, starting from 1.
|
||||
#[arg(long, short, value_name = "N", default_value = "1")]
|
||||
pub page: u64,
|
||||
}
|
||||
|
||||
impl QuerySubmissionCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let filter = {
|
||||
use io_jmap::types::email_submission::EmailSubmissionFilter;
|
||||
let f = EmailSubmissionFilter {
|
||||
undo_status: self.undo_status,
|
||||
before: self.before,
|
||||
after: self.after,
|
||||
..Default::default()
|
||||
};
|
||||
let has_one = f.undo_status.is_some() || f.before.is_some() || f.after.is_some();
|
||||
if has_one { Some(f) } else { None }
|
||||
};
|
||||
|
||||
let mut coroutine = QueryJmapEmailSubmissions::new(
|
||||
jmap.context,
|
||||
filter,
|
||||
None,
|
||||
Some(self.page.saturating_sub(1) * self.page_size),
|
||||
Some(self.page_size),
|
||||
)?;
|
||||
let mut arg = None;
|
||||
|
||||
let submissions = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
QueryJmapEmailSubmissionsResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
QueryJmapEmailSubmissionsResult::Ok { context, submissions, .. } => {
|
||||
jmap.context = context;
|
||||
break submissions;
|
||||
}
|
||||
QueryJmapEmailSubmissionsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
let table = SubmissionsTable {
|
||||
preset: account.table_preset,
|
||||
submissions,
|
||||
};
|
||||
|
||||
printer.out(table)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(transparent)]
|
||||
pub struct SubmissionsTable {
|
||||
#[serde(skip)]
|
||||
pub preset: String,
|
||||
pub submissions: Vec<EmailSubmission>,
|
||||
}
|
||||
|
||||
impl fmt::Display for SubmissionsTable {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut table = Table::new();
|
||||
|
||||
table
|
||||
.load_preset(&self.preset)
|
||||
.set_header(Row::from([
|
||||
Cell::new("ID"),
|
||||
Cell::new("EMAIL-ID"),
|
||||
Cell::new("IDENTITY-ID"),
|
||||
Cell::new("STATUS"),
|
||||
Cell::new("SENT-AT"),
|
||||
]))
|
||||
.add_rows(self.submissions.iter().map(|s| {
|
||||
Row::from([
|
||||
Cell::new(s.id.as_deref().unwrap_or("")),
|
||||
Cell::new(&s.email_id),
|
||||
Cell::new(&s.identity_id),
|
||||
Cell::new(s.undo_status.as_deref().unwrap_or("")),
|
||||
Cell::new(s.send_at.as_deref().unwrap_or("")),
|
||||
])
|
||||
}));
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{account::JmapAccount, thread::get::GetThreadCommand};
|
||||
|
||||
/// Manage JMAP threads.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum ThreadCommand {
|
||||
/// Fetch threads by ID (Thread/get).
|
||||
Get(GetThreadCommand),
|
||||
}
|
||||
|
||||
impl ThreadCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::thread_get::{GetJmapThreads, GetJmapThreadsResult};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Get JMAP threads by ID (Thread/get).
|
||||
///
|
||||
/// Each thread contains an ordered list of email IDs in the thread.
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetThreadCommand {
|
||||
/// Thread ID(s) to retrieve.
|
||||
#[arg(value_name = "ID", required = true)]
|
||||
pub ids: Vec<String>,
|
||||
}
|
||||
|
||||
impl GetThreadCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine = GetJmapThreads::new(jmap.context, self.ids.clone())?;
|
||||
let mut arg = None;
|
||||
|
||||
let (threads, not_found) = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapThreadsResult::Io(io) => arg = Some(handle(&mut jmap.stream, io)?),
|
||||
GetJmapThreadsResult::Ok {
|
||||
threads, not_found, ..
|
||||
} => break (threads, not_found),
|
||||
GetJmapThreadsResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
for id in ¬_found {
|
||||
printer.log(format!("Thread `{id}` not found."))?;
|
||||
}
|
||||
|
||||
for thread in threads {
|
||||
printer.out(serde_json::json!({
|
||||
"id": thread.id,
|
||||
"emailIds": thread.email_ids,
|
||||
}))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod command;
|
||||
pub mod get;
|
||||
@@ -0,0 +1,26 @@
|
||||
use anyhow::Result;
|
||||
use clap::Subcommand;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::{
|
||||
account::JmapAccount,
|
||||
vacation::{get::GetVacationCommand, set::SetVacationCommand},
|
||||
};
|
||||
|
||||
/// Manage JMAP vacation response.
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum VacationCommand {
|
||||
/// Get the vacation response (VacationResponse/get).
|
||||
Get(GetVacationCommand),
|
||||
/// Update the vacation response (VacationResponse/set).
|
||||
Set(SetVacationCommand),
|
||||
}
|
||||
|
||||
impl VacationCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
match self {
|
||||
Self::Get(cmd) => cmd.execute(printer, account),
|
||||
Self::Set(cmd) => cmd.execute(printer, account),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::coroutines::vacation_response_get::{
|
||||
GetJmapVacationResponse, GetJmapVacationResponseResult,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::Printer;
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Get the JMAP vacation response (VacationResponse/get).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct GetVacationCommand;
|
||||
|
||||
impl GetVacationCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let mut coroutine = GetJmapVacationResponse::new(jmap.context)?;
|
||||
let mut arg = None;
|
||||
|
||||
let vacation = loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
GetJmapVacationResponseResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
GetJmapVacationResponseResult::Ok { context, vacation_response, .. } => {
|
||||
jmap.context = context;
|
||||
break vacation_response;
|
||||
}
|
||||
GetJmapVacationResponseResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
};
|
||||
|
||||
match vacation {
|
||||
Some(v) => printer.out(serde_json::to_value(&v)?),
|
||||
None => printer.log("No vacation response configured."),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod command;
|
||||
pub mod get;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,85 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
use io_jmap::{
|
||||
coroutines::vacation_response_set::{
|
||||
SetJmapVacationResponse, SetJmapVacationResponseResult,
|
||||
},
|
||||
types::vacation_response::VacationResponseUpdate,
|
||||
};
|
||||
use io_stream::runtimes::std::handle;
|
||||
use pimalaya_toolbox::terminal::printer::{Message, Printer};
|
||||
|
||||
use crate::jmap::account::JmapAccount;
|
||||
|
||||
/// Update the JMAP vacation response (VacationResponse/set).
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetVacationCommand {
|
||||
/// Enable the vacation response.
|
||||
#[arg(long, conflicts_with = "disable")]
|
||||
pub enable: bool,
|
||||
|
||||
/// Disable the vacation response.
|
||||
#[arg(long, conflicts_with = "enable")]
|
||||
pub disable: bool,
|
||||
|
||||
/// Active from date (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub from_date: Option<String>,
|
||||
|
||||
/// Active until date (RFC 3339).
|
||||
#[arg(long, value_name = "DATE")]
|
||||
pub to_date: Option<String>,
|
||||
|
||||
/// Subject line for the auto-reply.
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub subject: Option<String>,
|
||||
|
||||
/// Plaintext body for the auto-reply.
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub text_body: Option<String>,
|
||||
|
||||
/// HTML body for the auto-reply.
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub html_body: Option<String>,
|
||||
}
|
||||
|
||||
impl SetVacationCommand {
|
||||
pub fn execute(self, printer: &mut impl Printer, account: JmapAccount) -> Result<()> {
|
||||
let mut jmap = account.new_jmap_session()?;
|
||||
|
||||
let is_enabled = if self.enable {
|
||||
Some(true)
|
||||
} else if self.disable {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let patch = VacationResponseUpdate {
|
||||
is_enabled,
|
||||
from_date: self.from_date,
|
||||
to_date: self.to_date,
|
||||
subject: self.subject,
|
||||
text_body: self.text_body,
|
||||
html_body: self.html_body,
|
||||
};
|
||||
|
||||
let mut coroutine = SetJmapVacationResponse::new(jmap.context, patch)?;
|
||||
let mut arg = None;
|
||||
|
||||
loop {
|
||||
match coroutine.resume(arg.take()) {
|
||||
SetJmapVacationResponseResult::Io(io) => {
|
||||
arg = Some(handle(&mut jmap.stream, io)?)
|
||||
}
|
||||
SetJmapVacationResponseResult::Ok { context, .. } => {
|
||||
jmap.context = context;
|
||||
break;
|
||||
}
|
||||
SetJmapVacationResponseResult::Err { err, .. } => bail!(err),
|
||||
}
|
||||
}
|
||||
|
||||
printer.out(Message::new("Vacation response updated."))
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ mod cli;
|
||||
mod config;
|
||||
#[cfg(feature = "imap")]
|
||||
mod imap;
|
||||
#[cfg(feature = "jmap")]
|
||||
mod jmap;
|
||||
#[cfg(feature = "maildir")]
|
||||
mod maildir;
|
||||
#[cfg(feature = "smtp")]
|
||||
|
||||
Reference in New Issue
Block a user