feat: init maildir support with message command

This commit is contained in:
Clément DOUIN
2026-03-16 16:06:15 +01:00
parent 7c4dcfc08a
commit dd43e0e123
22 changed files with 1060 additions and 209 deletions
Generated
+156 -170
View File
@@ -172,6 +172,12 @@ dependencies = [
"syn",
]
[[package]]
name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
[[package]]
name = "bytes"
version = "1.11.1"
@@ -328,7 +334,16 @@ checksum = "958c5d6ecf1f214b4c2bbbbf6ab9523a864bd136dcf71a7e8904799acfe1ad47"
dependencies = [
"crossterm",
"unicode-segmentation",
"unicode-width 0.2.2",
"unicode-width",
]
[[package]]
name = "convert_case"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49"
dependencies = [
"unicode-segmentation",
]
[[package]]
@@ -513,16 +528,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]]
name = "gethostname"
version = "1.1.0"
@@ -599,6 +604,17 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
[[package]]
name = "hashify"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "149e3ea90eb5a26ad354cfe3cb7f7401b9329032d0235f2687d03a35f30e5d4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "heck"
version = "0.5.0"
@@ -613,15 +629,18 @@ dependencies = [
"chrono",
"clap",
"comfy-table",
"convert_case",
"dirs",
"gethostname",
"html2text",
"io-fs",
"io-imap",
"io-maildir",
"io-process",
"io-smtp",
"io-stream",
"log",
"mail-parser",
"mime_guess",
"open",
"pimalaya-toolbox",
"rfc2047-decoder",
@@ -632,33 +651,6 @@ dependencies = [
"url",
]
[[package]]
name = "html2text"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"thiserror 1.0.69",
"unicode-width 0.1.13",
]
[[package]]
name = "html5ever"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -807,6 +799,15 @@ dependencies = [
"serde_core",
]
[[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-imap"
version = "0.0.1"
@@ -820,6 +821,20 @@ dependencies = [
"thiserror 2.0.18",
]
[[package]]
name = "io-maildir"
version = "0.0.1"
source = "git+https://github.com/pimalaya/io-maildir#e490e6ed98c2f5781b3e00319b61a4af7af15b1c"
dependencies = [
"gethostname",
"io-fs",
"log",
"mail-parser",
"memchr",
"thiserror 2.0.18",
"uuid",
]
[[package]]
name = "io-process"
version = "0.0.2"
@@ -943,6 +958,16 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.91"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -1021,33 +1046,14 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mail-parser"
version = "0.9.4"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93c3b9e5d8b17faf573330bbc43b37d6e918c0a3bf8a88e7d0a220ebc84af9fc"
checksum = "f82a3d6522697593ba4c683e0a6ee5a40fee93bc1a525e3cc6eeb3da11fd8897"
dependencies = [
"encoding_rs",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [
"log",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
"hashify",
"serde",
]
[[package]]
@@ -1065,6 +1071,22 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
@@ -1088,12 +1110,6 @@ dependencies = [
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nom"
version = "7.1.3"
@@ -1240,44 +1256,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
dependencies = [
"siphasher",
]
[[package]]
name = "pimalaya-toolbox"
version = "0.0.4"
@@ -1324,9 +1302,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
[[package]]
name = "portable-atomic-util"
version = "0.2.5"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5"
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
dependencies = [
"portable-atomic",
]
@@ -1349,12 +1327,6 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.37"
@@ -1630,6 +1602,12 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "same-file"
version = "1.0.6"
@@ -1769,12 +1747,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -1823,31 +1795,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "string_cache"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
"phf_shared",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]]
name = "strsim"
version = "0.11.1"
@@ -1895,17 +1842,6 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "tendril"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]]
name = "terminal_size"
version = "0.4.3"
@@ -2009,15 +1945,21 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]]
name = "uds_windows"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca"
checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.61.2",
]
[[package]]
name = "unicase"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -2030,12 +1972,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-width"
version = "0.2.2"
@@ -2067,12 +2003,6 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -2085,6 +2015,17 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
dependencies = [
"getrandom 0.4.2",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
@@ -2125,6 +2066,51 @@ dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
+9 -3
View File
@@ -16,10 +16,11 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["imap", "smtp", "rustls-ring"]
default = ["imap", "smtp", "maildir", "rustls-ring"]
imap = ["dep:io-imap", "pimalaya-toolbox/imap"]
smtp = ["dep:io-smtp", "pimalaya-toolbox/smtp"]
maildir = ["dep:convert_case", "dep:io-fs", "dep:io-maildir", "dep:mime_guess"]
native-tls = ["pimalaya-toolbox/native-tls"]
rustls-aws = ["pimalaya-toolbox/rustls-aws"]
@@ -35,15 +36,18 @@ anyhow = "1"
chrono = { version = "0.4", default-features = false }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
comfy-table = "7"
convert_case = { version = "0.11", optional = true }
dirs = "6"
gethostname = "1"
html2text = "0.12"
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-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 }
io-stream = { version = "0.0.2", default-features = false, features = ["std"] }
log = "0.4"
mail-parser = "0.9"
mail-parser = "0.11"
mime_guess = { version = "2", optional = true }
open = "5"
pimalaya-toolbox = { version = "0.0.4", default-features = false, features = ["config", "terminal", "secret"] }
rfc2047-decoder = "1"
@@ -56,7 +60,9 @@ url = { version = "2.2", features = ["serde"] }
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-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"
smtp-codec.git = "https://github.com/pimalaya/smtp-codec"
+18
View File
@@ -17,6 +17,8 @@ use pimalaya_toolbox::{
#[cfg(feature = "imap")]
use crate::imap::command::ImapCommand;
#[cfg(feature = "maildir")]
use crate::maildir::command::MaildirCommand;
#[cfg(feature = "smtp")]
use crate::smtp::command::SmtpCommand;
use crate::{account::Account, config::Config};
@@ -59,6 +61,9 @@ pub enum BackendCommand {
#[cfg(feature = "imap")]
#[command(subcommand)]
Imap(ImapCommand),
#[cfg(feature = "maildir")]
#[command(subcommand)]
Maildir(MaildirCommand),
#[cfg(feature = "smtp")]
#[command(subcommand)]
Smtp(SmtpCommand),
@@ -88,6 +93,19 @@ impl BackendCommand {
cmd.execute(printer, account)
}
#[cfg(feature = "maildir")]
Self::Maildir(cmd) => {
let config = Config::from_paths_or_default(config_paths)?;
let (account_name, mut account_config) = config.get_account(account_name)?;
let Some(maildir_config) = account_config.maildir.take() else {
bail!("Maildir config is missing for account `{account_name}`")
};
let account = Account::new(config, account_config, maildir_config)?;
cmd.execute(printer, account)
}
#[cfg(feature = "smtp")]
Self::Smtp(cmd) => {
let config = Config::from_paths_or_default(config_paths)?;
+8
View File
@@ -55,6 +55,7 @@ pub struct AccountConfig {
pub table_arrangement: Option<TableArrangementConfig>,
pub imap: Option<ImapConfig>,
pub maildir: Option<MaildirConfig>,
pub smtp: Option<SmtpConfig>,
}
@@ -90,6 +91,13 @@ pub struct ImapConfig {
pub sasl: SaslConfig,
}
/// Maildir configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct MaildirConfig {
pub root: PathBuf,
}
/// SMTP configuration.
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+1 -1
View File
@@ -12,7 +12,7 @@ use crate::imap::{
/// This command gives you access to the IMAP CLI API, and allows you
/// to manage IMAP mailboxes, envelopes, flags, messages etc.
#[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")]
#[command(rename_all = "kebab-case")]
pub enum ImapCommand {
Id(IdCommand),
+2 -2
View File
@@ -6,7 +6,7 @@ use crate::imap::{
account::ImapAccount,
message::{
copy::CopyMessageCommand, export::ExportMessageCommand, get::GetMessageCommand,
r#move::MoveMessageCommand, read::ReadMessageCommand, save::SaveMessageCommand,
r#move::MoveMessagesCommand, read::ReadMessageCommand, save::SaveMessageCommand,
},
};
@@ -22,7 +22,7 @@ pub enum MessageCommand {
Read(ReadMessageCommand),
Export(ExportMessageCommand),
Copy(CopyMessageCommand),
Move(MoveMessageCommand),
Move(MoveMessagesCommand),
}
impl MessageCommand {
+2 -2
View File
@@ -18,7 +18,7 @@ use crate::imap::{
/// from the source mailbox to the destination mailbox. Requires the
/// MOVE IMAP extension.
#[derive(Debug, Parser)]
pub struct MoveMessageCommand {
pub struct MoveMessagesCommand {
#[command(flatten)]
pub mailbox_name: MailboxNameOptionalFlag,
#[command(flatten)]
@@ -35,7 +35,7 @@ pub struct MoveMessageCommand {
pub seq: bool,
}
impl MoveMessageCommand {
impl MoveMessagesCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mailbox = self.mailbox_name.inner.try_into()?;
+42 -30
View File
@@ -1,13 +1,13 @@
use std::{fmt, num::NonZeroU32};
use anyhow::{anyhow, bail, Result};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
coroutines::{fetch::*, select::*},
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use io_stream::runtimes::std::handle;
use mail_parser::MessageParser;
use mail_parser::{Message, MessageParser};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
@@ -99,47 +99,59 @@ impl ReadMessageCommand {
};
let Some(message) = MessageParser::new().parse(&raw) else {
bail!("Invalid message");
bail!("Invalid MIME message");
};
let content = if self.html {
message
.body_html(0)
.map(|s| s.to_string())
.ok_or_else(|| anyhow!("No HTML content found"))?
if self.html {
printer.out(MessageHtmlView { message })
} else {
if let Some(text) = message.body_text(0) {
text.to_string()
} else if let Some(html) = message.body_html(0) {
html2text::from_read(html.as_bytes(), self.width)
} else {
bail!("No text or HTML content found");
}
};
let output = MessageContent { content };
printer.out(output)
printer.out(MessagePlainView { message })
}
}
}
#[derive(Clone, Debug)]
pub struct MessageContent {
pub content: String,
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessagePlainView<'a> {
message: Message<'a>,
}
impl fmt::Display for MessageContent {
impl fmt::Display for MessagePlainView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f)?;
write!(f, "{}", self.content)?;
if !self.content.ends_with('\n') {
writeln!(f)?;
for (i, part) in self.message.text_bodies().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
if let Some(contents) = part.text_contents() {
write!(f, "{}", contents.trim_end())?;
}
}
Ok(())
}
}
impl Serialize for MessageContent {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.content.serialize(serializer)
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessageHtmlView<'a> {
message: Message<'a>,
}
impl fmt::Display for MessageHtmlView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, part) in self.message.html_bodies().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
if let Some(contents) = part.text_contents() {
write!(f, "{}", contents.trim_end())?;
}
}
Ok(())
}
}
+3
View File
@@ -0,0 +1,3 @@
use crate::{account::Account, config::MaildirConfig};
pub type MaildirAccount = Account<MaildirConfig>;
+66
View File
@@ -0,0 +1,66 @@
use clap::Parser;
const INBOX: &str = "INBOX";
/// The optional maildir name argument parser.
#[derive(Debug, Parser)]
pub struct MaildirNameOptionalArg {
/// The name of the maildir.
#[arg(name = "maildir_name", value_name = "MAILDIR", default_value = INBOX)]
pub inner: String,
}
impl Default for MaildirNameOptionalArg {
fn default() -> Self {
Self {
inner: INBOX.into(),
}
}
}
/// The optional maildir name flag parser.
#[derive(Debug, Parser)]
pub struct MaildirPathOptionalFlag {
/// The name of the maildir.
#[arg(long = "maildir", short = 'm')]
#[arg(name = "maildir_name", value_name = "NAME", default_value = INBOX)]
pub inner: String,
}
impl Default for MaildirPathOptionalFlag {
fn default() -> Self {
Self {
inner: INBOX.into(),
}
}
}
#[derive(Debug, Parser)]
pub struct MaildirNoSelectFlag {
/// Do not select the given maildir before performing the current
/// action.
///
/// This argument is useful when stateful IMAP sessions are used,
/// for example with Sirup CLI:
///
/// https://github.com/pimalaya/sirup
#[arg(long = "no-select", default_value_t)]
#[arg(name = "maildir_no_select")]
pub inner: bool,
}
/// The required maildir name argument parser.
#[derive(Debug, Parser)]
pub struct MaildirNameArg {
/// The name of the maildir.
#[arg(name = "maildir_name", value_name = "MAILDIR")]
pub inner: String,
}
/// The target maildir name argument parser.
#[derive(Debug, Clone, Parser)]
pub struct TargetMaildirNameArg {
/// The name of the target maildir.
#[arg(name = "target_maildir_name", value_name = "TARGET")]
pub inner: String,
}
+25
View File
@@ -0,0 +1,25 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::maildir::{account::MaildirAccount, message::command::MessageCommand};
/// MAILDIR CLI (requires the `maildir` cargo feature).
///
/// This command gives you access to the MAILDIR CLI API, and allows you
/// to manage MAILDIR mailboxes, envelopes, flags, messages etc.
#[derive(Debug, Subcommand)]
#[command(rename_all = "kebab-case")]
pub enum MaildirCommand {
#[command(subcommand)]
#[command(aliases = ["msgs", "msg"])]
Messages(MessageCommand),
}
impl MaildirCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
match self {
Self::Messages(cmd) => cmd.execute(printer, account),
}
}
}
+39
View File
@@ -0,0 +1,39 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use crate::maildir::{
account::MaildirAccount,
message::{
copy::CopyMessagesCommand, export::ExportMessageCommand, get::GetMessageCommand,
r#move::MoveMessagesCommand, read::ReadMessageCommand, save::SaveMessageCommand,
},
};
/// Manage MAILDIR messages.
///
/// A message is a complete email including headers and body. This
/// subcommand allows you to save, get, read, export, copy, and move
/// messages.
#[derive(Debug, Subcommand)]
pub enum MessageCommand {
Save(SaveMessageCommand),
Get(GetMessageCommand),
Read(ReadMessageCommand),
Export(ExportMessageCommand),
Copy(CopyMessagesCommand),
Move(MoveMessagesCommand),
}
impl MessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
match self {
Self::Save(cmd) => cmd.execute(printer, account),
Self::Get(cmd) => cmd.execute(printer, account),
Self::Read(cmd) => cmd.execute(printer, account),
Self::Export(cmd) => cmd.execute(printer, account),
Self::Copy(cmd) => cmd.execute(printer, account),
Self::Move(cmd) => cmd.execute(printer, account),
}
}
}
+87
View File
@@ -0,0 +1,87 @@
use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use io_fs::runtimes::std::handle;
use io_maildir::{
coroutines::copy_message::*,
maildir::{Maildir, MaildirSubdir},
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::maildir::account::MaildirAccount;
/// Copy Maildir message to the given mailbox.
///
/// This command copies message(s) identified by the given sequence
/// set from the source mailbox to the destination mailbox.
#[derive(Debug, Parser)]
pub struct CopyMessagesCommand {
/// Path to the source Maildir, where messages are copied from.
#[arg(long = "source", short = 's')]
#[arg(value_name = "PATH", default_value = "Inbox")]
pub maildir_source_path: PathBuf,
/// Path to the target Maildir, where messages are copied into.
#[arg(long = "target", short = 't')]
#[arg(value_name = "PATH")]
pub maildir_target_path: PathBuf,
/// Subdir of the target Maildir.
#[arg(long = "subdir", value_name = "NAME", value_enum)]
pub maildir_target_subdir: MaildirSubdirArg,
/// Id(s) of message(s) to copy.
#[arg(value_name = "ID", num_args = 1..)]
pub ids: Vec<String>,
}
impl CopyMessagesCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir_source = match Maildir::try_from(self.maildir_source_path.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_source_path))?,
};
let maildir_target = match Maildir::try_from(self.maildir_target_path.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_target_path))?,
};
for id in self.ids {
let mut arg = None;
let mut coroutine = CopyMaildirMessage::new(
maildir_source.clone(),
maildir_target.clone(),
self.maildir_target_subdir.clone().into(),
id,
);
loop {
match coroutine.resume(arg.take()) {
CopyMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
CopyMaildirMessageResult::Ok => break,
CopyMaildirMessageResult::Err(err) => bail!(err),
}
}
}
printer.out(Message::new("Message(s) successfully copied"))
}
}
#[derive(Clone, Debug, ValueEnum)]
pub enum MaildirSubdirArg {
Cur,
New,
Tmp,
}
impl From<MaildirSubdirArg> for MaildirSubdir {
fn from(value: MaildirSubdirArg) -> Self {
match value {
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
MaildirSubdirArg::New => MaildirSubdir::New,
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
}
}
}
+165
View File
@@ -0,0 +1,165 @@
use std::{fmt, fs, path::PathBuf};
use anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use convert_case::ccase;
use io_fs::runtimes::std::handle;
use io_maildir::{coroutines::get_message::*, maildir::Maildir, types::MimeHeaders};
use mime_guess::{get_mime_extensions_str, mime::OCTET_STREAM};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::maildir::account::MaildirAccount;
/// Export type for message export.
#[derive(Clone, Debug, Default, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum ExportType {
#[default]
/// Output raw RFC822 message to stdout.
Raw,
/// Export all MIME parts to separate files.
Parts,
}
/// Export a message.
///
/// This command exports a message in various formats:
/// - raw: Output raw RFC822 message to stdout
/// - eml: Save as .eml file
/// - parts: Export all MIME parts to separate files
#[derive(Debug, Parser)]
pub struct ExportMessageCommand {
/// Path to the Maildir containing the message looked for.
#[arg(long = "maildir", short)]
#[arg(value_name = "PATH", default_value = "Inbox")]
pub maildir_path: PathBuf,
/// Id of message to export.
#[arg()]
pub id: String,
/// Type of the export.
#[arg(long, short, value_enum, default_value_t)]
pub r#type: ExportType,
/// Output directory (for eml and parts types).
#[arg(long, short, value_name = "DIR")]
pub directory: Option<PathBuf>,
/// Open exported content in default application, when applicable.
#[arg(long, short)]
pub open: bool,
}
impl ExportMessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir = match Maildir::try_from(self.maildir_path.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_path))?,
};
let mut arg = None;
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
let msg = loop {
match coroutine.resume(arg.take()) {
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
GetMaildirMessageResult::Ok(msg) => break msg,
GetMaildirMessageResult::Err(err) => bail!(err),
};
};
match self.r#type {
ExportType::Raw => {
let contents = String::from_utf8(msg.into())?;
printer.out(ExportRaw { contents })?;
}
ExportType::Parts => {
let Some(msg) = msg.parsed() else {
bail!("Invalid MIME message at {}", msg.path().display());
};
let dir = self.directory.unwrap_or_else(|| PathBuf::from(self.id));
fs::create_dir_all(&dir)?;
let mut parts = Vec::new();
for (i, part) in msg.parts.iter().enumerate() {
let cr = part.content_type().map(|ct| match &ct.c_subtype {
Some(sub) => format!("{}/{}", ct.c_type, sub),
None => ct.c_type.to_string(),
});
if let Some(ref ct) = cr {
if ct.starts_with("multipart/") {
continue;
}
}
let filename = match part.attachment_name() {
Some(name) => ccase!(kebab, name),
None => {
let ext = match cr.as_deref().unwrap_or(OCTET_STREAM.as_str()) {
"text/plain" => Some(&"txt"),
"text/html" => Some(&"html"),
ct => get_mime_extensions_str(ct).and_then(|ext| ext.first()),
};
match ext {
Some(ext) => format!("part_{i}.{ext}"),
None => format!("part_{i}"),
}
}
};
let path = dir.join(&filename);
let contents = part.contents();
fs::write(&path, contents)?;
parts.push(path);
}
if self.open {
for path in &parts {
if let Some(ext) = path.extension() {
if ext == "html" {
open::that(path)?;
}
}
}
}
printer.out(ExportParts { parts })?;
}
};
Ok(())
}
}
#[derive(Serialize)]
struct ExportRaw {
contents: String,
}
impl fmt::Display for ExportRaw {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.contents)
}
}
#[derive(Serialize)]
struct ExportParts {
parts: Vec<PathBuf>,
}
impl fmt::Display for ExportParts {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for path in &self.parts {
writeln!(f, " - {}", path.display())?;
}
writeln!(f)?;
write!(f, "Exported {} part(s)", self.parts.len())
}
}
+88
View File
@@ -0,0 +1,88 @@
use std::{fmt, path::PathBuf};
use anyhow::{bail, Result};
use clap::Parser;
use io_fs::runtimes::std::handle;
use io_maildir::{
coroutines::get_message::*,
maildir::Maildir,
types::{Message, PartType},
};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::maildir::account::MaildirAccount;
/// Get Maildir message to the given mailbox.
///
/// This command copies message(s) identified by the given sequence
/// set from the source mailbox to the destination mailbox.
#[derive(Debug, Parser)]
pub struct GetMessageCommand {
/// Path to the Maildir containing the message looked for.
#[arg(long, short, value_name = "PATH")]
#[arg(default_value = "Inbox")]
pub maildir: PathBuf,
/// Id of message to get.
#[arg(value_name = "ID")]
pub id: String,
}
impl GetMessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir = match Maildir::try_from(self.maildir.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
};
let mut arg = None;
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
let msg = loop {
match coroutine.resume(arg.take()) {
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
GetMaildirMessageResult::Ok(msg) => break msg,
GetMaildirMessageResult::Err(err) => bail!(err),
};
};
let Some(msg) = msg.parsed() else {
bail!("Invalid MIME message at {}", msg.path().display());
};
printer.out(MessageView(msg))
}
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessageView<'a>(Message<'a>);
impl fmt::Display for MessageView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts_len = self.0.parts.len();
for (i, p) in self.0.parts.iter().enumerate() {
writeln!(f, "---")?;
writeln!(f, "Part {}/{parts_len}:", i + 1)?;
writeln!(f)?;
for h in p.headers() {
writeln!(f, "{}: {:?}", h.name.as_str(), h.value)?;
}
writeln!(f)?;
match &p.body {
PartType::Text(p) => writeln!(f, "{p}")?,
PartType::Html(p) => writeln!(f, "{p}")?,
PartType::Binary(p) => writeln!(f, "({} bytes)", p.len())?,
PartType::InlineBinary(p) => writeln!(f, "({} inline bytes)", p.len())?,
PartType::Multipart(_) => continue,
PartType::Message(m) => write!(f, "{}", MessageView(m.clone()))?,
}
}
Ok(())
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod command;
pub mod copy;
pub mod export;
pub mod get;
pub mod r#move;
pub mod read;
pub mod save;
+87
View File
@@ -0,0 +1,87 @@
use std::path::PathBuf;
use anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use io_fs::runtimes::std::handle;
use io_maildir::{
coroutines::move_message::*,
maildir::{Maildir, MaildirSubdir},
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use crate::maildir::account::MaildirAccount;
/// Move Maildir message to the given mailbox.
///
/// This command copies message(s) identified by the given sequence
/// set from the source mailbox to the destination mailbox.
#[derive(Debug, Parser)]
pub struct MoveMessagesCommand {
/// Path to the source Maildir, where messages are copied from.
#[arg(long = "source", short = 's')]
#[arg(value_name = "PATH", default_value = "Inbox")]
pub maildir_source_path: PathBuf,
/// Path to the target Maildir, where messages are copied into.
#[arg(long = "target", short = 't')]
#[arg(value_name = "PATH")]
pub maildir_target_path: PathBuf,
/// Subdir of the target Maildir.
#[arg(long = "subdir", value_name = "NAME", value_enum)]
pub maildir_target_subdir: MaildirSubdirArg,
/// Id(s) of message(s) to move.
#[arg(value_name = "ID", num_args = 1..)]
pub ids: Vec<String>,
}
impl MoveMessagesCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir_source = match Maildir::try_from(self.maildir_source_path.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_source_path))?,
};
let maildir_target = match Maildir::try_from(self.maildir_target_path.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir_target_path))?,
};
for id in self.ids {
let mut arg = None;
let mut coroutine = MoveMaildirMessage::new(
maildir_source.clone(),
maildir_target.clone(),
self.maildir_target_subdir.clone().into(),
id,
);
loop {
match coroutine.resume(arg.take()) {
MoveMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
MoveMaildirMessageResult::Ok => break,
MoveMaildirMessageResult::Err(err) => bail!(err),
}
}
}
printer.out(Message::new("Message(s) successfully copied"))
}
}
#[derive(Clone, Debug, ValueEnum)]
pub enum MaildirSubdirArg {
Cur,
New,
Tmp,
}
impl From<MaildirSubdirArg> for MaildirSubdir {
fn from(value: MaildirSubdirArg) -> Self {
match value {
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
MaildirSubdirArg::New => MaildirSubdir::New,
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
}
}
}
+110
View File
@@ -0,0 +1,110 @@
use std::{fmt, path::PathBuf};
use anyhow::{bail, Result};
use clap::Parser;
use io_fs::runtimes::std::handle;
use io_maildir::{coroutines::get_message::*, maildir::Maildir, types::Message};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::maildir::account::MaildirAccount;
/// Read message content.
///
/// This command fetches a message and displays its text content.
/// By default it shows plain text content; use --html to show HTML.
#[derive(Debug, Parser)]
pub struct ReadMessageCommand {
/// Path to the Maildir containing the message looked for.
#[arg(long, short, value_name = "PATH")]
#[arg(default_value = "Inbox")]
pub maildir: PathBuf,
/// Id of message to read.
#[arg()]
pub id: String,
/// Show HTML content instead of plain text.
#[arg(long)]
pub html: bool,
/// Terminal width for text wrapping.
#[arg(long, short, default_value = "80")]
pub width: usize,
}
impl ReadMessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir = match Maildir::try_from(self.maildir.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
};
let mut arg = None;
let mut coroutine = GetMaildirMessage::new(maildir, &self.id);
let message = loop {
match coroutine.resume(arg.take()) {
GetMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
GetMaildirMessageResult::Ok(msg) => break msg,
GetMaildirMessageResult::Err(err) => bail!(err),
};
};
let Some(message) = message.parsed() else {
bail!("Invalid MIME message at {}", message.path().display());
};
if self.html {
printer.out(MessageHtmlView { message })
} else {
printer.out(MessagePlainView { message })
}
}
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessagePlainView<'a> {
message: Message<'a>,
}
impl fmt::Display for MessagePlainView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, part) in self.message.text_bodies().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
if let Some(contents) = part.text_contents() {
write!(f, "{}", contents.trim_end())?;
}
}
Ok(())
}
}
#[derive(Serialize)]
#[serde(transparent)]
pub struct MessageHtmlView<'a> {
message: Message<'a>,
}
impl fmt::Display for MessageHtmlView<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, part) in self.message.html_bodies().enumerate() {
if i > 0 {
writeln!(f)?;
writeln!(f)?;
}
if let Some(contents) = part.text_contents() {
write!(f, "{}", contents.trim_end())?;
}
}
Ok(())
}
}
+138
View File
@@ -0,0 +1,138 @@
use std::{
fmt,
io::{stdin, BufRead, IsTerminal},
path::PathBuf,
};
use anyhow::{bail, Result};
use clap::{Parser, ValueEnum};
use io_fs::runtimes::std::handle;
use io_maildir::{
coroutines::store_message::*,
flag::Flag,
maildir::{Maildir, MaildirSubdir},
};
use pimalaya_toolbox::terminal::printer::Printer;
use serde::Serialize;
use crate::maildir::account::MaildirAccount;
/// Save a message to a mailbox.
///
/// This command appends a message to the specified mailbox. The
/// message is read from stdin in RFC 5322 format (raw email).
#[derive(Debug, Parser)]
pub struct SaveMessageCommand {
/// Path to the Maildir to save message into.
#[arg(long, short, value_name = "PATH")]
#[arg(default_value = "Inbox")]
pub maildir: PathBuf,
/// The subdirectory of the Maildir
#[arg(long, short, value_name = "NAME", value_enum)]
#[arg(default_value = "new")]
pub subdir: MaildirSubdirArg,
/// The flags to add to the message.
#[arg(long = "flag", short, num_args = 0..)]
pub flags: Vec<FlagArg>,
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message", value_name = "MESSAGE")]
pub message: Vec<String>,
}
impl SaveMessageCommand {
pub fn execute(self, printer: &mut impl Printer, account: MaildirAccount) -> Result<()> {
let maildir = match Maildir::try_from(self.maildir.clone()) {
Ok(maildir) => maildir,
Err(_) => Maildir::try_from(account.backend.root.join(self.maildir))?,
};
let msg = if stdin().is_terminal() || printer.is_json() {
self.message
.join(" ")
.replace('\r', "")
.replace('\n', "\r\n")
} else {
stdin()
.lock()
.lines()
.map_while(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let flags = self.flags.into_iter().map(Into::into).into();
let mut arg = None;
let mut coroutine =
StoreMaildirMessage::new(maildir, self.subdir.into(), flags, msg.into_bytes());
let out = loop {
match coroutine.resume(arg.take()) {
StoreMaildirMessageResult::Io(io) => arg = Some(handle(io)?),
StoreMaildirMessageResult::Ok { id, path } => break StoredMessage { id, path },
StoreMaildirMessageResult::Err(err) => bail!(err),
}
};
printer.out(out)
}
}
#[derive(Clone, Debug, ValueEnum)]
pub enum MaildirSubdirArg {
Cur,
New,
Tmp,
}
impl From<MaildirSubdirArg> for MaildirSubdir {
fn from(value: MaildirSubdirArg) -> Self {
match value {
MaildirSubdirArg::Cur => MaildirSubdir::Cur,
MaildirSubdirArg::New => MaildirSubdir::New,
MaildirSubdirArg::Tmp => MaildirSubdir::Tmp,
}
}
}
#[derive(Clone, Debug, ValueEnum)]
#[clap(rename_all = "kebab-case")]
pub enum FlagArg {
Passed,
Replied,
Seen,
Trashed,
Draft,
Flagged,
}
impl From<FlagArg> for Flag {
fn from(flag: FlagArg) -> Self {
match flag {
FlagArg::Passed => Flag::Passed,
FlagArg::Replied => Flag::Replied,
FlagArg::Seen => Flag::Seen,
FlagArg::Trashed => Flag::Trashed,
FlagArg::Draft => Flag::Draft,
FlagArg::Flagged => Flag::Flagged,
}
}
}
#[derive(Serialize)]
pub struct StoredMessage {
id: String,
path: PathBuf,
}
impl fmt::Display for StoredMessage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let id = &self.id;
let path = self.path.display();
write!(f, "Message `{id}` successfully saved to {path}")
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod account;
pub mod arg;
pub mod command;
pub mod message;
+2
View File
@@ -3,6 +3,8 @@ mod cli;
mod config;
#[cfg(feature = "imap")]
mod imap;
#[cfg(feature = "maildir")]
mod maildir;
#[cfg(feature = "smtp")]
mod smtp;
+1 -1
View File
@@ -10,7 +10,7 @@ use crate::smtp::{account::SmtpAccount, message::command::MessageCommand};
/// you to manage SMTP mailboxes: list mailboxes, read messages,
/// add flags etc.
#[derive(Debug, Subcommand)]
#[command(rename_all = "lowercase")]
#[command(rename_all = "kebab-case")]
pub enum SmtpCommand {
#[command(subcommand)]
#[command(aliases = ["msgs", "msg"])]