mirror of
https://github.com/soywod/himalaya.git
synced 2024-07-05 17:15:12 +00:00
Merge pull request #462 from soywod/backend-features
`v1.0.0-beta` ready for testing 🎉
This commit is contained in:
commit
b623468d15
3463
Cargo.lock
generated
3463
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
184
Cargo.toml
184
Cargo.toml
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "himalaya"
|
name = "himalaya"
|
||||||
description = "CLI to manage emails."
|
description = "CLI to manage emails"
|
||||||
version = "0.9.0"
|
version = "1.0.0-beta"
|
||||||
authors = ["soywod <clement.douin@posteo.net>"]
|
authors = ["soywod <clement.douin@posteo.net>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -10,139 +10,71 @@ keywords = ["cli", "mail", "email", "client", "imap"]
|
||||||
homepage = "https://pimalaya.org/himalaya"
|
homepage = "https://pimalaya.org/himalaya"
|
||||||
documentation = "https://pimalaya.org/himalaya/"
|
documentation = "https://pimalaya.org/himalaya/"
|
||||||
repository = "https://github.com/soywod/himalaya/"
|
repository = "https://github.com/soywod/himalaya/"
|
||||||
metadata.docs.rs.all-features = true
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = [
|
default = [
|
||||||
"imap-backend",
|
"maildir",
|
||||||
# "notmuch-backend",
|
"imap",
|
||||||
"smtp-sender",
|
# "notmuch",
|
||||||
|
"smtp",
|
||||||
|
"sendmail",
|
||||||
# "pgp-commands",
|
# "pgp-commands",
|
||||||
# "pgp-gpg",
|
# "pgp-gpg",
|
||||||
# "pgp-native",
|
# "pgp-native",
|
||||||
]
|
]
|
||||||
imap-backend = ["email-lib/imap-backend"]
|
maildir = ["email-lib/maildir"]
|
||||||
notmuch-backend = ["email-lib/notmuch-backend"]
|
imap = ["email-lib/imap"]
|
||||||
smtp-sender = ["email-lib/smtp-sender"]
|
notmuch = ["email-lib/notmuch"]
|
||||||
|
smtp = ["email-lib/smtp"]
|
||||||
|
sendmail = ["email-lib/sendmail"]
|
||||||
pgp = []
|
pgp = []
|
||||||
pgp-commands = ["pgp", "email-lib/pgp-commands"]
|
pgp-commands = ["pgp", "mml-lib/pgp-commands", "email-lib/pgp-commands"]
|
||||||
pgp-gpg = ["pgp", "email-lib/pgp-gpg"]
|
pgp-gpg = ["pgp", "mml-lib/pgp-gpg", "email-lib/pgp-gpg"]
|
||||||
pgp-native = ["pgp", "email-lib/pgp-native"]
|
pgp-native = ["pgp", "mml-lib/pgp-native", "email-lib/pgp-native"]
|
||||||
|
|
||||||
# dev dependencies
|
[dev-dependencies]
|
||||||
|
async-trait = "0.1"
|
||||||
|
tempfile = "3.3"
|
||||||
|
|
||||||
[dev-dependencies.async-trait]
|
[dependencies]
|
||||||
version = "0.1"
|
anyhow = "1"
|
||||||
|
async-trait = "0.1"
|
||||||
[dev-dependencies.tempfile]
|
chrono = "0.4.24"
|
||||||
version = "3.3"
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
|
clap_complete = "4.4"
|
||||||
# dependencies
|
clap_mangen = "0.2"
|
||||||
|
console = "0.15.2"
|
||||||
[dependencies.anyhow]
|
dialoguer = "0.10.2"
|
||||||
version = "1.0"
|
dirs = "4.0"
|
||||||
|
email-lib = { version = "=0.17.1", default-features = false }
|
||||||
[dependencies.atty]
|
email_address = "0.2.4"
|
||||||
version = "0.2"
|
env_logger = "0.8"
|
||||||
|
erased-serde = "0.3"
|
||||||
[dependencies.chrono]
|
indicatif = "0.17"
|
||||||
version = "0.4.24"
|
keyring-lib = "=0.3.0"
|
||||||
|
log = "0.4"
|
||||||
[dependencies.clap]
|
mail-builder = "0.3"
|
||||||
version = "4.0"
|
md5 = "0.7.0"
|
||||||
|
mml-lib = { version = "=1.0.3", default-features = false }
|
||||||
[dependencies.clap_complete]
|
oauth-lib = "=0.1.0"
|
||||||
version = "4.0"
|
once_cell = "1.16"
|
||||||
|
process-lib = "=0.3.0"
|
||||||
[dependencies.clap_mangen]
|
secret-lib = "=0.3.0"
|
||||||
version = "0.2"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
[dependencies.console]
|
shellexpand-utils = "=0.2.0"
|
||||||
version = "0.15.2"
|
termcolor = "1.1"
|
||||||
|
terminal_size = "0.1"
|
||||||
[dependencies.dialoguer]
|
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
|
||||||
version = "0.10.2"
|
toml = "0.7.4"
|
||||||
|
toml_edit = "0.19.8"
|
||||||
[dependencies.dirs]
|
unicode-width = "0.1"
|
||||||
version = "4.0.0"
|
url = "2.2"
|
||||||
|
uuid = { version = "0.8", features = ["v4"] }
|
||||||
[dependencies.email_address]
|
|
||||||
version = "0.2.4"
|
|
||||||
|
|
||||||
[dependencies.env_logger]
|
|
||||||
version = "0.8"
|
|
||||||
|
|
||||||
[dependencies.erased-serde]
|
|
||||||
version = "0.3"
|
|
||||||
|
|
||||||
[dependencies.indicatif]
|
|
||||||
version = "0.17"
|
|
||||||
|
|
||||||
[dependencies.log]
|
|
||||||
version = "0.4"
|
|
||||||
|
|
||||||
[dependencies.md5]
|
|
||||||
version = "0.7.0"
|
|
||||||
|
|
||||||
[dependencies.once_cell]
|
|
||||||
version = "1.16.0"
|
|
||||||
|
|
||||||
[dependencies.email-lib]
|
|
||||||
version = "=0.15.3"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[dependencies.keyring-lib]
|
|
||||||
version = "=0.1.0"
|
|
||||||
|
|
||||||
[dependencies.oauth-lib]
|
|
||||||
version = "=0.1.0"
|
|
||||||
|
|
||||||
[dependencies.process-lib]
|
|
||||||
version = "=0.1.0"
|
|
||||||
|
|
||||||
[dependencies.mml-lib]
|
|
||||||
version = "=0.5.0"
|
|
||||||
default-features = false
|
|
||||||
|
|
||||||
[dependencies.secret-lib]
|
|
||||||
version = "=0.1.0"
|
|
||||||
|
|
||||||
[dependencies.serde]
|
|
||||||
version = "1.0"
|
|
||||||
features = ["derive"]
|
|
||||||
|
|
||||||
[dependencies.serde_json]
|
|
||||||
version = "1.0"
|
|
||||||
|
|
||||||
[dependencies.shellexpand-utils]
|
|
||||||
version = "=0.1.0"
|
|
||||||
|
|
||||||
[dependencies.termcolor]
|
|
||||||
version = "1.1"
|
|
||||||
|
|
||||||
[dependencies.terminal_size]
|
|
||||||
version = "0.1"
|
|
||||||
|
|
||||||
[dependencies.tokio]
|
|
||||||
version = "1.23"
|
|
||||||
default-features = false
|
|
||||||
features = ["macros", "rt-multi-thread"]
|
|
||||||
|
|
||||||
[dependencies.toml]
|
|
||||||
version = "0.7.4"
|
|
||||||
|
|
||||||
[dependencies.toml_edit]
|
|
||||||
version = "0.19.8"
|
|
||||||
|
|
||||||
[dependencies.unicode-width]
|
|
||||||
version = "0.1"
|
|
||||||
|
|
||||||
[dependencies.url]
|
|
||||||
version = "2.2"
|
|
||||||
|
|
||||||
[dependencies.uuid]
|
|
||||||
version = "0.8"
|
|
||||||
features = ["v4"]
|
|
||||||
|
|
||||||
[target.'cfg(target_env = "musl")'.dependencies.rusqlite]
|
[target.'cfg(target_env = "musl")'.dependencies.rusqlite]
|
||||||
version = "0.29"
|
version = "0.29"
|
||||||
|
@ -153,4 +85,4 @@ version = "0.29"
|
||||||
features = ["bundled"]
|
features = ["bundled"]
|
||||||
|
|
||||||
[target.'cfg(not(windows))'.dependencies.coredump]
|
[target.'cfg(not(windows))'.dependencies.coredump]
|
||||||
version = "=0.1.2"
|
version = "=0.1.2"
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022 soywod <clement.douin@posteo.net>
|
Copyright (c) 2022-2023 soywod <clement.douin@posteo.net>
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
29
README.md
29
README.md
|
@ -85,35 +85,22 @@ Please read the [documentation](https://pimalaya.org/himalaya/cli/configuration/
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you find a **bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
If you want to **report a bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||||
|
|
||||||
If you have a **question**, please send an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
|
If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) using [git send-email](https://git-scm.com/docs/git-send-email). Follow [this guide](https://git-send-email.io/) to configure git properly.
|
||||||
|
|
||||||
If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) using [git send-email](https://git-scm.com/docs/git-send-email) (see [this guide](https://git-send-email.io/) on how to configure it).
|
If you just want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.general](https://matrix.to/#/#pimalaya.general:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org). You can also use the mailing list [[send an email](mailto:~soywod/pimalaya@lists.sr.ht)|[subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht)|[unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht)].
|
||||||
|
|
||||||
If you want to **subscribe** to the mailing list, please send an email at [~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht).
|
## Sponsoring
|
||||||
|
|
||||||
If you want to **unsubscribe** to the mailing list, please send an email at [~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
|
|
||||||
|
|
||||||
If you want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org).
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
|
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
|
||||||
|
|
||||||
Special thanks to the [nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that helped Himalaya to receive financial support from the [NGI Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the European Commission in September, 2022.
|
Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from:
|
||||||
|
|
||||||
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
- [NGI Assure](https://nlnet.nl/assure/) in 2022
|
||||||
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
|
- [NGI Zero Untrust](https://nlnet.nl/entrust/) in 2023
|
||||||
- [isync](https://isync.sourceforge.io/), an email synchronizer for offline usage
|
|
||||||
- [NeoMutt](https://neomutt.org/), an email terminal user interface
|
|
||||||
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other email terminal user interface
|
|
||||||
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool over NeoMutt and isync
|
|
||||||
- [rust-imap](https://github.com/jonhoo/rust-imap), a Rust IMAP library
|
|
||||||
- [lettre](https://github.com/lettre/lettre), a Rust mailer library
|
|
||||||
- [mailparse](https://github.com/staktrace/mailparse), a Rust MIME email parser.
|
|
||||||
|
|
||||||
## Sponsoring
|
If you appreciate the project, feel free to donate using one of the following providers:
|
||||||
|
|
||||||
[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod)
|
[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod)
|
||||||
[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod)
|
[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod)
|
||||||
|
|
|
@ -1,54 +1,77 @@
|
||||||
display-name = "Display NAME"
|
|
||||||
signature-delim = "~~"
|
|
||||||
signature = "~/.signature"
|
|
||||||
downloads-dir = "~/downloads"
|
|
||||||
folder-listing-page-size = 12
|
|
||||||
email-listing-page-size = 12
|
|
||||||
email-reading-headers = ["From", "To"]
|
|
||||||
email-reading-verify-cmd = "gpg --verify -q"
|
|
||||||
email-reading-decrypt-cmd = "gpg -dq"
|
|
||||||
email-writing-sign-cmd = "gpg -o - -saq"
|
|
||||||
email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>"
|
|
||||||
|
|
||||||
[example]
|
[example]
|
||||||
|
# Make this account the default one to use when no account is given to
|
||||||
|
# commands.
|
||||||
default = true
|
default = true
|
||||||
display-name = "Display NAME (gmail)"
|
|
||||||
email = "display.name@gmail.local"
|
|
||||||
|
|
||||||
|
# The display-name and the email are used to build the full email
|
||||||
|
# address: "My example account" <example@localhost>
|
||||||
|
display-name = "My example account"
|
||||||
|
email = "example@localhost"
|
||||||
|
|
||||||
|
# The signature can be a string or a path to a file.
|
||||||
|
signature = "Regards,"
|
||||||
|
signature-delim = "-- \n"
|
||||||
|
|
||||||
|
# Enable the synchronization for this account. Running the command
|
||||||
|
# `account sync example` will synchronize all folders and all emails
|
||||||
|
# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`.
|
||||||
|
sync.enable = true
|
||||||
|
|
||||||
|
# Override the default Maildir path for synchronization.
|
||||||
|
sync.dir = "/tmp/himalaya-sync-example"
|
||||||
|
|
||||||
|
# Define main folder aliases
|
||||||
|
folder.alias.inbox = "INBOX"
|
||||||
|
folder.alias.sent = "Sent"
|
||||||
|
folder.alias.drafts = "Drafts"
|
||||||
|
folder.alias.trash = "Trash"
|
||||||
|
|
||||||
|
# Also define custom folder aliases
|
||||||
|
folder.alias.prev-year = "Archives/2023"
|
||||||
|
|
||||||
|
# Default backend used for all the features like adding folders,
|
||||||
|
# listing envelopes or copying messages.
|
||||||
backend = "imap"
|
backend = "imap"
|
||||||
imap-host = "imap.gmail.com"
|
|
||||||
imap-login = "display.name@gmail.local"
|
|
||||||
imap-auth = "passwd"
|
|
||||||
imap-passwd.cmd = "pass show gmail"
|
|
||||||
imap-port = 993
|
|
||||||
imap-ssl = true
|
|
||||||
imap-starttls = false
|
|
||||||
imap-notify-cmd = """📫 "<sender>" "<subject>""""
|
|
||||||
imap-notify-query = "NOT SEEN"
|
|
||||||
imap-watch-cmds = ["echo \"received server changes!\""]
|
|
||||||
|
|
||||||
sender = "smtp"
|
envelope.list.page-size = 10
|
||||||
smtp-host = "smtp.gmail.com"
|
envelope.list.datetime-fmt = "%F %R%:z"
|
||||||
smtp-login = "display.name@gmail.local"
|
|
||||||
smtp-auth = "passwd"
|
|
||||||
smtp-passwd.cmd = "pass show piana/gmail"
|
|
||||||
smtp-port = 465
|
|
||||||
smtp-ssl = true
|
|
||||||
smtp-starttls = false
|
|
||||||
|
|
||||||
sync = true
|
# Date are converted to the user's local timezone.
|
||||||
sync-dir = "/tmp/sync/gmail"
|
envelope.list.datetime-local-tz = true
|
||||||
sync-folders-strategy.include = ["INBOX"]
|
|
||||||
|
|
||||||
[example.folder-aliases]
|
# Override the backend used for listing envelopes.
|
||||||
inbox = "INBOX"
|
envelope.list.backend = "imap"
|
||||||
drafts = "[Gmail]/Drafts"
|
|
||||||
sent = "[Gmail]/Sent Mail"
|
|
||||||
trash = "[Gmail]/Trash"
|
|
||||||
|
|
||||||
[example.email-hooks]
|
# Override the backend used for sending messages.
|
||||||
pre-send = "echo $1"
|
message.send.backend = "smtp"
|
||||||
|
|
||||||
[example.email-reading-format]
|
# IMAP config
|
||||||
type = "fixed"
|
imap.host = "localhost"
|
||||||
width = 64
|
imap.port = 3143
|
||||||
|
imap.login = "example@localhost"
|
||||||
|
imap.ssl = false
|
||||||
|
imap.starttls = false
|
||||||
|
imap.auth = "passwd" # or oauth2
|
||||||
|
|
||||||
|
# Get password from the raw string (not safe)
|
||||||
|
# imap.passwd.raw = "password"
|
||||||
|
|
||||||
|
# Get password from a shell command
|
||||||
|
imap.passwd.cmd = ["echo example-imap-password", "cat"]
|
||||||
|
|
||||||
|
# Get password from your system keyring using secret service
|
||||||
|
# Keyring secrets can be (re)set with the command `account configure example`
|
||||||
|
# imap.passwd.keyring = "example-imap-password"
|
||||||
|
|
||||||
|
# SMTP config
|
||||||
|
smtp.host = "localhost"
|
||||||
|
smtp.port = 3025
|
||||||
|
smtp.login = "example@localhost"
|
||||||
|
smtp.ssl = false
|
||||||
|
smtp.starttls = false
|
||||||
|
smtp.auth = "passwd"
|
||||||
|
smtp.passwd.raw = "password"
|
||||||
|
|
||||||
|
# PGP needs to be enabled with one of those cargo feature:
|
||||||
|
# pgp-commands, pgp-gpg or pgp-native
|
||||||
|
# pgp.backend = "gpg"
|
||||||
|
|
1
src/account/arg/mod.rs
Normal file
1
src/account/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod name;
|
24
src/account/arg/name.rs
Normal file
24
src/account/arg/name.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The account name argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct AccountNameArg {
|
||||||
|
/// The name of the account.
|
||||||
|
///
|
||||||
|
/// An account name corresponds to an entry in the table at the
|
||||||
|
/// root level of your TOML configuration file.
|
||||||
|
#[arg(name = "account_name", value_name = "ACCOUNT")]
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The account name flag parser.
|
||||||
|
#[derive(Debug, Default, Parser)]
|
||||||
|
pub struct AccountNameFlag {
|
||||||
|
/// Override the default account.
|
||||||
|
///
|
||||||
|
/// An account name corresponds to an entry in the table at the
|
||||||
|
/// root level of your TOML configuration file.
|
||||||
|
#[arg(long = "account", short = 'a')]
|
||||||
|
#[arg(name = "account_name", value_name = "NAME")]
|
||||||
|
pub name: Option<String>,
|
||||||
|
}
|
116
src/account/command/configure.rs
Normal file
116
src/account/command/configure.rs
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use email::imap::config::ImapAuthConfig;
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use email::smtp::config::SmtpAuthConfig;
|
||||||
|
use log::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameArg,
|
||||||
|
config::{
|
||||||
|
wizard::{prompt_passwd, prompt_secret},
|
||||||
|
TomlConfig,
|
||||||
|
},
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Configure an account.
|
||||||
|
///
|
||||||
|
/// This command is mostly used to define or reset passwords managed
|
||||||
|
/// by your global keyring. If you do not use the keyring system, you
|
||||||
|
/// can skip this command.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct AccountConfigureCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameArg,
|
||||||
|
|
||||||
|
/// Reset keyring passwords.
|
||||||
|
///
|
||||||
|
/// This argument will force passwords to be prompted again, then
|
||||||
|
/// saved to your global keyring.
|
||||||
|
#[arg(long, short)]
|
||||||
|
pub reset: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountConfigureCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing account configure command");
|
||||||
|
|
||||||
|
let account = &self.account.name;
|
||||||
|
let (_, account_config) = config.into_toml_account_config(Some(account))?;
|
||||||
|
|
||||||
|
if self.reset {
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
if let Some(ref config) = account_config.imap {
|
||||||
|
let reset = match &config.auth {
|
||||||
|
ImapAuthConfig::Passwd(config) => config.reset().await,
|
||||||
|
ImapAuthConfig::OAuth2(config) => config.reset().await,
|
||||||
|
};
|
||||||
|
if let Err(err) = reset {
|
||||||
|
warn!("error while resetting imap secrets: {err}");
|
||||||
|
debug!("error while resetting imap secrets: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
if let Some(ref config) = account_config.smtp {
|
||||||
|
let reset = match &config.auth {
|
||||||
|
SmtpAuthConfig::Passwd(config) => config.reset().await,
|
||||||
|
SmtpAuthConfig::OAuth2(config) => config.reset().await,
|
||||||
|
};
|
||||||
|
if let Err(err) = reset {
|
||||||
|
warn!("error while resetting smtp secrets: {err}");
|
||||||
|
debug!("error while resetting smtp secrets: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
if let Some(ref config) = account_config.pgp {
|
||||||
|
config.reset().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
if let Some(ref config) = account_config.imap {
|
||||||
|
match &config.auth {
|
||||||
|
ImapAuthConfig::Passwd(config) => {
|
||||||
|
config.configure(|| prompt_passwd("IMAP password")).await
|
||||||
|
}
|
||||||
|
ImapAuthConfig::OAuth2(config) => {
|
||||||
|
config
|
||||||
|
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
if let Some(ref config) = account_config.smtp {
|
||||||
|
match &config.auth {
|
||||||
|
SmtpAuthConfig::Passwd(config) => {
|
||||||
|
config.configure(|| prompt_passwd("SMTP password")).await
|
||||||
|
}
|
||||||
|
SmtpAuthConfig::OAuth2(config) => {
|
||||||
|
config
|
||||||
|
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
if let Some(ref config) = account_config.pgp {
|
||||||
|
config
|
||||||
|
.configure(&account_config.email, || {
|
||||||
|
prompt_passwd("PGP secret key password")
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Account {account} successfully {}configured!",
|
||||||
|
if self.reset { "re" } else { "" }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
139
src/account/command/list.rs
Normal file
139
src/account/command/list.rs
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::Accounts,
|
||||||
|
config::TomlConfig,
|
||||||
|
printer::{PrintTableOpts, Printer},
|
||||||
|
ui::arg::max_width::TableMaxWidthFlag,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// List all accounts.
|
||||||
|
///
|
||||||
|
/// This command lists all accounts defined in your TOML configuration
|
||||||
|
/// file.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct AccountListCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub table: TableMaxWidthFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountListCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing account list command");
|
||||||
|
|
||||||
|
let accounts: Accounts = config.accounts.iter().into();
|
||||||
|
|
||||||
|
printer.print_table(
|
||||||
|
Box::new(accounts),
|
||||||
|
PrintTableOpts {
|
||||||
|
format: &Default::default(),
|
||||||
|
max_width: self.table.max_width,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use email::{account::config::AccountConfig, imap::config::ImapConfig};
|
||||||
|
use std::{collections::HashMap, fmt::Debug, io};
|
||||||
|
use termcolor::ColorSpec;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::TomlAccountConfig,
|
||||||
|
backend::BackendKind,
|
||||||
|
printer::{Print, PrintTable, WriteColor},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_should_match_cmds_accounts() {
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
struct StringWriter {
|
||||||
|
content: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl io::Write for StringWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||||
|
self.content
|
||||||
|
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
self.content = String::default();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl termcolor::WriteColor for StringWriter {
|
||||||
|
fn supports_color(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
||||||
|
io::Result::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) -> io::Result<()> {
|
||||||
|
io::Result::Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WriteColor for StringWriter {}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct PrinterServiceTest {
|
||||||
|
pub writer: StringWriter,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Printer for PrinterServiceTest {
|
||||||
|
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||||
|
&mut self,
|
||||||
|
data: Box<T>,
|
||||||
|
opts: PrintTableOpts,
|
||||||
|
) -> Result<()> {
|
||||||
|
data.print_table(&mut self.writer, opts)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
fn is_json(&self) -> bool {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut printer = PrinterServiceTest::default();
|
||||||
|
let config = AccountConfig::default();
|
||||||
|
let deserialized_config = TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account-1".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
default: Some(true),
|
||||||
|
backend: Some(BackendKind::Imap),
|
||||||
|
imap: Some(ImapConfig::default()),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
||||||
|
assert_eq!(
|
||||||
|
concat![
|
||||||
|
"\n",
|
||||||
|
"NAME │BACKEND │DEFAULT \n",
|
||||||
|
"account-1 │imap │yes \n",
|
||||||
|
"\n"
|
||||||
|
],
|
||||||
|
printer.writer.content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
39
src/account/command/mod.rs
Normal file
39
src/account/command/mod.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
mod configure;
|
||||||
|
mod list;
|
||||||
|
mod sync;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::{config::TomlConfig, printer::Printer};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
configure::AccountConfigureCommand, list::AccountListCommand, sync::AccountSyncCommand,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manage accounts.
|
||||||
|
///
|
||||||
|
/// An account is a set of settings, identified by an account
|
||||||
|
/// name. Settings are directly taken from your TOML configuration
|
||||||
|
/// file. This subcommand allows you to manage them.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum AccountSubcommand {
|
||||||
|
#[command(alias = "cfg")]
|
||||||
|
Configure(AccountConfigureCommand),
|
||||||
|
|
||||||
|
#[command(alias = "lst")]
|
||||||
|
List(AccountListCommand),
|
||||||
|
|
||||||
|
#[command(alias = "synchronize", alias = "synchronise")]
|
||||||
|
Sync(AccountSyncCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountSubcommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Configure(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Sync(cmd) => cmd.execute(printer, config).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
258
src/account/command/sync.rs
Normal file
258
src/account/command/sync.rs
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{ArgAction, Parser};
|
||||||
|
use email::{
|
||||||
|
account::sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
||||||
|
folder::sync::FolderSyncStrategy,
|
||||||
|
};
|
||||||
|
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
||||||
|
use log::info;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Mutex,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameArg, backend::BackendBuilder, config::TomlConfig,
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||||
|
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||||
|
ProgressStyle::with_template(
|
||||||
|
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
||||||
|
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Synchronize an account.
|
||||||
|
///
|
||||||
|
/// This command allows you to synchronize all folders and emails
|
||||||
|
/// (including envelopes and messages) of a given account into a local
|
||||||
|
/// Maildir folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct AccountSyncCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameArg,
|
||||||
|
|
||||||
|
/// Run the synchronization without applying any changes.
|
||||||
|
///
|
||||||
|
/// Instead, a report will be printed to stdout containing all the
|
||||||
|
/// changes the synchronization plan to do.
|
||||||
|
#[arg(long, short)]
|
||||||
|
pub dry_run: bool,
|
||||||
|
|
||||||
|
/// Synchronize only specific folders.
|
||||||
|
///
|
||||||
|
/// Only the given folders will be synchronized (including
|
||||||
|
/// associated envelopes and messages). Useful when you need to
|
||||||
|
/// speed up the synchronization process. A good usecase is to
|
||||||
|
/// synchronize only the INBOX in order to quickly check for new
|
||||||
|
/// messages.
|
||||||
|
#[arg(long, short = 'f')]
|
||||||
|
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
|
||||||
|
#[arg(conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
|
||||||
|
pub include_folder: Vec<String>,
|
||||||
|
|
||||||
|
/// Omit specific folders from the synchronization.
|
||||||
|
///
|
||||||
|
/// The given folders will be excluded from the synchronization
|
||||||
|
/// (including associated envelopes and messages). Useful when you
|
||||||
|
/// have heavy folders that you do not want to take care of, or to
|
||||||
|
/// speed up the synchronization process.
|
||||||
|
#[arg(long, short = 'x')]
|
||||||
|
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
|
||||||
|
#[arg(conflicts_with = "include_folder", conflicts_with = "all_folders")]
|
||||||
|
pub exclude_folder: Vec<String>,
|
||||||
|
|
||||||
|
/// Synchronize all exsting folders.
|
||||||
|
#[arg(long, short = 'A')]
|
||||||
|
#[arg(conflicts_with = "include_folder", conflicts_with = "exclude_folder")]
|
||||||
|
pub all_folders: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountSyncCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing account sync command");
|
||||||
|
|
||||||
|
let included_folders = HashSet::from_iter(self.include_folder);
|
||||||
|
let excluded_folders = HashSet::from_iter(self.exclude_folder);
|
||||||
|
|
||||||
|
let strategy = if !included_folders.is_empty() {
|
||||||
|
Some(FolderSyncStrategy::Include(included_folders))
|
||||||
|
} else if !excluded_folders.is_empty() {
|
||||||
|
Some(FolderSyncStrategy::Exclude(excluded_folders))
|
||||||
|
} else if self.all_folders {
|
||||||
|
Some(FolderSyncStrategy::All)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) = config
|
||||||
|
.clone()
|
||||||
|
.into_account_configs(Some(self.account.name.as_str()), true)?;
|
||||||
|
|
||||||
|
let backend_builder =
|
||||||
|
BackendBuilder::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
let sync_builder = AccountSyncBuilder::new(backend_builder.into())
|
||||||
|
.await?
|
||||||
|
.with_some_folders_strategy(strategy)
|
||||||
|
.with_dry_run(self.dry_run);
|
||||||
|
|
||||||
|
if self.dry_run {
|
||||||
|
let report = sync_builder.sync().await?;
|
||||||
|
let mut hunks_count = report.folders_patch.len();
|
||||||
|
|
||||||
|
if !report.folders_patch.is_empty() {
|
||||||
|
printer.print_log("Folders patch:")?;
|
||||||
|
for (hunk, _) in report.folders_patch {
|
||||||
|
printer.print_log(format!(" - {hunk}"))?;
|
||||||
|
}
|
||||||
|
printer.print_log("")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !report.emails_patch.is_empty() {
|
||||||
|
printer.print_log("Envelopes patch:")?;
|
||||||
|
for (hunk, _) in report.emails_patch {
|
||||||
|
hunks_count += 1;
|
||||||
|
printer.print_log(format!(" - {hunk}"))?;
|
||||||
|
}
|
||||||
|
printer.print_log("")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Estimated patch length for account {} to be synchronized: {hunks_count}",
|
||||||
|
self.account.name
|
||||||
|
))?;
|
||||||
|
} else if printer.is_json() {
|
||||||
|
sync_builder.sync().await?;
|
||||||
|
printer.print(format!(
|
||||||
|
"Account {} successfully synchronized!",
|
||||||
|
self.account.name
|
||||||
|
))?;
|
||||||
|
} else {
|
||||||
|
let multi = MultiProgress::new();
|
||||||
|
let sub_progresses = Mutex::new(HashMap::new());
|
||||||
|
let main_progress = multi.add(
|
||||||
|
ProgressBar::new(100)
|
||||||
|
.with_style(MAIN_PROGRESS_STYLE.clone())
|
||||||
|
.with_message("Synchronizing folders…"),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force the progress bar to show
|
||||||
|
main_progress.set_position(0);
|
||||||
|
|
||||||
|
let report = sync_builder
|
||||||
|
.with_on_progress(move |evt| {
|
||||||
|
use AccountSyncProgressEvent::*;
|
||||||
|
Ok(match evt {
|
||||||
|
ApplyFolderPatches(..) => {
|
||||||
|
main_progress.inc(3);
|
||||||
|
}
|
||||||
|
ApplyEnvelopePatches(patches) => {
|
||||||
|
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
||||||
|
let patches_len =
|
||||||
|
patches.values().fold(0, |sum, patch| sum + patch.len());
|
||||||
|
main_progress.set_length((110 * patches_len / 100) as u64);
|
||||||
|
main_progress.set_position((5 * patches_len / 100) as u64);
|
||||||
|
main_progress.set_message("Synchronizing envelopes…");
|
||||||
|
|
||||||
|
for (folder, patch) in patches {
|
||||||
|
let progress = ProgressBar::new(patch.len() as u64)
|
||||||
|
.with_style(SUB_PROGRESS_STYLE.clone())
|
||||||
|
.with_prefix(folder.clone())
|
||||||
|
.with_finish(ProgressFinish::AndClear);
|
||||||
|
let progress = multi.add(progress);
|
||||||
|
envelopes_progresses.insert(folder, progress.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplyEnvelopeHunk(hunk) => {
|
||||||
|
main_progress.inc(1);
|
||||||
|
let mut progresses = sub_progresses.lock().unwrap();
|
||||||
|
if let Some(progress) = progresses.get_mut(hunk.folder()) {
|
||||||
|
progress.inc(1);
|
||||||
|
if progress.position() == (progress.length().unwrap() - 1) {
|
||||||
|
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
|
||||||
|
} else {
|
||||||
|
progress.set_message(format!("{hunk}…"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ApplyEnvelopeCachePatch(_patch) => {
|
||||||
|
main_progress.set_length(100);
|
||||||
|
main_progress.set_position(95);
|
||||||
|
main_progress.set_message("Saving cache database…");
|
||||||
|
}
|
||||||
|
ExpungeFolders(folders) => {
|
||||||
|
let mut progresses = sub_progresses.lock().unwrap();
|
||||||
|
for progress in progresses.values() {
|
||||||
|
progress.finish_and_clear()
|
||||||
|
}
|
||||||
|
progresses.clear();
|
||||||
|
|
||||||
|
main_progress.set_position(100);
|
||||||
|
main_progress
|
||||||
|
.set_message(format!("Expunging {} folders…", folders.len()));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.sync()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let folders_patch_err = report
|
||||||
|
.folders_patch
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !folders_patch_err.is_empty() {
|
||||||
|
printer.print_log("")?;
|
||||||
|
printer.print_log("Errors occurred while applying the folders patch:")?;
|
||||||
|
folders_patch_err
|
||||||
|
.iter()
|
||||||
|
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = report.folders_cache_patch.1 {
|
||||||
|
printer.print_log("")?;
|
||||||
|
printer.print_log(format!(
|
||||||
|
"Error occurred while applying the folder cache patch: {err}"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let envelopes_patch_err = report
|
||||||
|
.emails_patch
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !envelopes_patch_err.is_empty() {
|
||||||
|
printer.print_log("")?;
|
||||||
|
printer.print_log("Errors occurred while applying the envelopes patch:")?;
|
||||||
|
for (hunk, err) in folders_patch_err {
|
||||||
|
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(err) = report.emails_cache_patch.1 {
|
||||||
|
printer.print_log("")?;
|
||||||
|
printer.print_log(format!(
|
||||||
|
"Error occurred while applying the envelopes cache patch: {err}"
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Account {} successfully synchronized!",
|
||||||
|
self.account.name
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
219
src/account/config.rs
Normal file
219
src/account/config.rs
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
//! Deserialized account config module.
|
||||||
|
//!
|
||||||
|
//! This module contains the raw deserialized representation of an
|
||||||
|
//! account in the accounts section of the user configuration file.
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
use email::account::config::pgp::PgpConfig;
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use email::imap::config::ImapConfig;
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use email::smtp::config::SmtpConfig;
|
||||||
|
use email::{
|
||||||
|
account::sync::config::SyncConfig, maildir::config::MaildirConfig,
|
||||||
|
sendmail::config::SendmailConfig,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig,
|
||||||
|
folder::config::FolderConfig, message::config::MessageConfig,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Represents all existing kind of account config.
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct TomlAccountConfig {
|
||||||
|
pub default: Option<bool>,
|
||||||
|
|
||||||
|
pub email: String,
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub signature: Option<String>,
|
||||||
|
pub signature_delim: Option<String>,
|
||||||
|
pub downloads_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
pub sync: Option<SyncConfig>,
|
||||||
|
pub folder: Option<FolderConfig>,
|
||||||
|
pub envelope: Option<EnvelopeConfig>,
|
||||||
|
pub flag: Option<FlagConfig>,
|
||||||
|
pub message: Option<MessageConfig>,
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
pub pgp: Option<PgpConfig>,
|
||||||
|
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
#[cfg(feature = "maildir")]
|
||||||
|
pub maildir: Option<MaildirConfig>,
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
pub imap: Option<ImapConfig>,
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
pub notmuch: Option<NotmuchConfig>,
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
pub smtp: Option<SmtpConfig>,
|
||||||
|
#[cfg(feature = "sendmail")]
|
||||||
|
pub sendmail: Option<SendmailConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TomlAccountConfig {
|
||||||
|
pub fn add_folder_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.folder
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|folder| folder.add.as_ref())
|
||||||
|
.and_then(|add| add.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_folders_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.folder
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|folder| folder.list.as_ref())
|
||||||
|
.and_then(|list| list.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expunge_folder_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.folder
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|folder| folder.expunge.as_ref())
|
||||||
|
.and_then(|expunge| expunge.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn purge_folder_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.folder
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|folder| folder.purge.as_ref())
|
||||||
|
.and_then(|purge| purge.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_folder_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.folder
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|folder| folder.delete.as_ref())
|
||||||
|
.and_then(|delete| delete.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_envelope_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.envelope
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|envelope| envelope.get.as_ref())
|
||||||
|
.and_then(|get| get.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_envelopes_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.envelope
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|envelope| envelope.list.as_ref())
|
||||||
|
.and_then(|list| list.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_flags_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.flag
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|flag| flag.add.as_ref())
|
||||||
|
.and_then(|add| add.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_flags_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.flag
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|flag| flag.set.as_ref())
|
||||||
|
.and_then(|set| set.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_flags_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.flag
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|flag| flag.remove.as_ref())
|
||||||
|
.and_then(|remove| remove.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_raw_message_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|msg| msg.write.as_ref())
|
||||||
|
.and_then(|add| add.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek_messages_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|message| message.peek.as_ref())
|
||||||
|
.and_then(|peek| peek.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_messages_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|message| message.read.as_ref())
|
||||||
|
.and_then(|get| get.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn copy_messages_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|message| message.copy.as_ref())
|
||||||
|
.and_then(|copy| copy.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn move_messages_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|message| message.move_.as_ref())
|
||||||
|
.and_then(|move_| move_.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_messages_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.flag
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|flag| flag.remove.as_ref())
|
||||||
|
.and_then(|remove| remove.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_raw_message_kind(&self) -> Option<&BackendKind> {
|
||||||
|
self.message
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|msg| msg.send.as_ref())
|
||||||
|
.and_then(|send| send.backend.as_ref())
|
||||||
|
.or_else(|| self.backend.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut used_backends = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(ref kind) = self.backend {
|
||||||
|
used_backends.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref folder) = self.folder {
|
||||||
|
used_backends.extend(folder.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref envelope) = self.envelope {
|
||||||
|
used_backends.extend(envelope.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref flag) = self.flag {
|
||||||
|
used_backends.extend(flag.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref msg) = self.message {
|
||||||
|
used_backends.extend(msg.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
used_backends
|
||||||
|
}
|
||||||
|
}
|
132
src/account/mod.rs
Normal file
132
src/account/mod.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
pub mod arg;
|
||||||
|
pub mod command;
|
||||||
|
pub mod config;
|
||||||
|
pub(crate) mod wizard;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{collections::hash_map::Iter, fmt, ops::Deref};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||||
|
ui::table::{Cell, Row, Table},
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::config::TomlAccountConfig;
|
||||||
|
|
||||||
|
/// Represents the printable account.
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct Account {
|
||||||
|
/// Represents the account name.
|
||||||
|
pub name: String,
|
||||||
|
/// Represents the backend name of the account.
|
||||||
|
pub backend: String,
|
||||||
|
/// Represents the default state of the account.
|
||||||
|
pub default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Account {
|
||||||
|
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
backend: backend.into(),
|
||||||
|
default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Account {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table for Account {
|
||||||
|
fn head() -> Row {
|
||||||
|
Row::new()
|
||||||
|
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||||
|
.cell(Cell::new("BACKENDS").bold().underline().white())
|
||||||
|
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row(&self) -> Row {
|
||||||
|
let default = if self.default { "yes" } else { "" };
|
||||||
|
Row::new()
|
||||||
|
.cell(Cell::new(&self.name).shrinkable().green())
|
||||||
|
.cell(Cell::new(&self.backend).blue())
|
||||||
|
.cell(Cell::new(default).white())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents the list of printable accounts.
|
||||||
|
#[derive(Debug, Default, Serialize)]
|
||||||
|
pub struct Accounts(pub Vec<Account>);
|
||||||
|
|
||||||
|
impl Deref for Accounts {
|
||||||
|
type Target = Vec<Account>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrintTable for Accounts {
|
||||||
|
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||||
|
writeln!(writer)?;
|
||||||
|
Table::print(writer, self, opts)?;
|
||||||
|
writeln!(writer)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
|
||||||
|
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
|
||||||
|
let mut accounts: Vec<_> = map
|
||||||
|
.map(|(name, account)| {
|
||||||
|
let mut backends = String::new();
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
if account.imap.is_some() {
|
||||||
|
backends.push_str("imap");
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.maildir.is_some() {
|
||||||
|
if !backends.is_empty() {
|
||||||
|
backends.push_str(", ")
|
||||||
|
}
|
||||||
|
backends.push_str("maildir");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
if account.imap.is_some() {
|
||||||
|
if !backends.is_empty() {
|
||||||
|
backends.push_str(", ")
|
||||||
|
}
|
||||||
|
backends.push_str("notmuch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
if account.smtp.is_some() {
|
||||||
|
if !backends.is_empty() {
|
||||||
|
backends.push_str(", ")
|
||||||
|
}
|
||||||
|
backends.push_str("smtp");
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.sendmail.is_some() {
|
||||||
|
if !backends.is_empty() {
|
||||||
|
backends.push_str(", ")
|
||||||
|
}
|
||||||
|
backends.push_str("sendmail");
|
||||||
|
}
|
||||||
|
|
||||||
|
Account::new(name, &backends, account.default.unwrap_or_default())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// sort accounts by name
|
||||||
|
accounts.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
|
||||||
|
|
||||||
|
Self(accounts)
|
||||||
|
}
|
||||||
|
}
|
107
src/account/wizard.rs
Normal file
107
src/account/wizard.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use dialoguer::{Confirm, Input};
|
||||||
|
use email::account::sync::config::SyncConfig;
|
||||||
|
use email_address::EmailAddress;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::{self, config::BackendConfig, BackendKind},
|
||||||
|
config::wizard::THEME,
|
||||||
|
message::config::{MessageConfig, MessageSendConfig},
|
||||||
|
wizard_prompt,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::TomlAccountConfig;
|
||||||
|
|
||||||
|
pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
|
||||||
|
let mut config = TomlAccountConfig::default();
|
||||||
|
|
||||||
|
let account_name = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Account name")
|
||||||
|
.default(String::from("Personal"))
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
config.email = Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Email address")
|
||||||
|
.validate_with(|email: &String| {
|
||||||
|
if EmailAddress::is_valid(email) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
bail!("Invalid email address: {email}")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.interact()?;
|
||||||
|
|
||||||
|
config.display_name = Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Full display name")
|
||||||
|
.interact()?,
|
||||||
|
);
|
||||||
|
|
||||||
|
config.downloads_dir = Some(
|
||||||
|
Input::with_theme(&*THEME)
|
||||||
|
.with_prompt("Downloads directory")
|
||||||
|
.default(String::from("~/Downloads"))
|
||||||
|
.interact()?
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
match backend::wizard::configure(&account_name, &config.email).await? {
|
||||||
|
Some(BackendConfig::Maildir(mdir_config)) => {
|
||||||
|
config.maildir = Some(mdir_config);
|
||||||
|
config.backend = Some(BackendKind::Maildir);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendConfig::Imap(imap_config)) => {
|
||||||
|
config.imap = Some(imap_config);
|
||||||
|
config.backend = Some(BackendKind::Imap);
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendConfig::Notmuch(notmuch_config)) => {
|
||||||
|
config.notmuch = Some(notmuch_config);
|
||||||
|
config.backend = Some(BackendKind::Notmuch);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
match backend::wizard::configure_sender(&account_name, &config.email).await? {
|
||||||
|
Some(BackendConfig::Sendmail(sendmail_config)) => {
|
||||||
|
config.sendmail = Some(sendmail_config);
|
||||||
|
config.message = Some(MessageConfig {
|
||||||
|
send: Some(MessageSendConfig {
|
||||||
|
backend: Some(BackendKind::Sendmail),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Some(BackendConfig::Smtp(smtp_config)) => {
|
||||||
|
config.smtp = Some(smtp_config);
|
||||||
|
config.message = Some(MessageConfig {
|
||||||
|
send: Some(MessageSendConfig {
|
||||||
|
backend: Some(BackendKind::Smtp),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
let should_configure_sync = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Do you need an offline access to your account?"
|
||||||
|
))
|
||||||
|
.default(false)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if should_configure_sync {
|
||||||
|
config.sync = Some(SyncConfig {
|
||||||
|
enable: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some((account_name, config)))
|
||||||
|
}
|
19
src/backend/config.rs
Normal file
19
src/backend/config.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use email::imap::config::ImapConfig;
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
use email::notmuch::config::NotmuchConfig;
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use email::smtp::config::SmtpConfig;
|
||||||
|
use email::{maildir::config::MaildirConfig, sendmail::config::SendmailConfig};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub enum BackendConfig {
|
||||||
|
Maildir(MaildirConfig),
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Imap(ImapConfig),
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Notmuch(NotmuchConfig),
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Smtp(SmtpConfig),
|
||||||
|
Sendmail(SendmailConfig),
|
||||||
|
}
|
832
src/backend/mod.rs
Normal file
832
src/backend/mod.rs
Normal file
|
@ -0,0 +1,832 @@
|
||||||
|
pub mod config;
|
||||||
|
pub(crate) mod wizard;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use email::imap::{ImapSessionBuilder, ImapSessionSync};
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use email::smtp::{SmtpClientBuilder, SmtpClientSync};
|
||||||
|
use email::{
|
||||||
|
account::config::AccountConfig,
|
||||||
|
envelope::{
|
||||||
|
get::{imap::GetEnvelopeImap, maildir::GetEnvelopeMaildir},
|
||||||
|
list::{imap::ListEnvelopesImap, maildir::ListEnvelopesMaildir},
|
||||||
|
Id, SingleId,
|
||||||
|
},
|
||||||
|
flag::{
|
||||||
|
add::{imap::AddFlagsImap, maildir::AddFlagsMaildir},
|
||||||
|
remove::{imap::RemoveFlagsImap, maildir::RemoveFlagsMaildir},
|
||||||
|
set::{imap::SetFlagsImap, maildir::SetFlagsMaildir},
|
||||||
|
Flag, Flags,
|
||||||
|
},
|
||||||
|
folder::{
|
||||||
|
add::{imap::AddFolderImap, maildir::AddFolderMaildir},
|
||||||
|
delete::{imap::DeleteFolderImap, maildir::DeleteFolderMaildir},
|
||||||
|
expunge::{imap::ExpungeFolderImap, maildir::ExpungeFolderMaildir},
|
||||||
|
list::{imap::ListFoldersImap, maildir::ListFoldersMaildir},
|
||||||
|
purge::imap::PurgeFolderImap,
|
||||||
|
},
|
||||||
|
maildir::{config::MaildirConfig, MaildirSessionBuilder, MaildirSessionSync},
|
||||||
|
message::{
|
||||||
|
add_raw::imap::AddRawMessageImap,
|
||||||
|
add_raw_with_flags::{
|
||||||
|
imap::AddRawMessageWithFlagsImap, maildir::AddRawMessageWithFlagsMaildir,
|
||||||
|
},
|
||||||
|
copy::{imap::CopyMessagesImap, maildir::CopyMessagesMaildir},
|
||||||
|
get::imap::GetMessagesImap,
|
||||||
|
move_::{imap::MoveMessagesImap, maildir::MoveMessagesMaildir},
|
||||||
|
peek::{imap::PeekMessagesImap, maildir::PeekMessagesMaildir},
|
||||||
|
send_raw::{sendmail::SendRawMessageSendmail, smtp::SendRawMessageSmtp},
|
||||||
|
Messages,
|
||||||
|
},
|
||||||
|
sendmail::SendmailContext,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum BackendKind {
|
||||||
|
Maildir,
|
||||||
|
#[serde(skip_deserializing)]
|
||||||
|
MaildirForSync,
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Imap,
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Notmuch,
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Smtp,
|
||||||
|
Sendmail,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToString for BackendKind {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let kind = match self {
|
||||||
|
Self::Maildir => "Maildir",
|
||||||
|
Self::MaildirForSync => "Maildir",
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Self::Imap => "IMAP",
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Self::Notmuch => "Notmuch",
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Self::Smtp => "SMTP",
|
||||||
|
Self::Sendmail => "Sendmail",
|
||||||
|
};
|
||||||
|
|
||||||
|
kind.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct BackendContextBuilder {
|
||||||
|
maildir: Option<MaildirSessionBuilder>,
|
||||||
|
maildir_for_sync: Option<MaildirSessionBuilder>,
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
imap: Option<ImapSessionBuilder>,
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
smtp: Option<SmtpClientBuilder>,
|
||||||
|
sendmail: Option<SendmailContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl email::backend::BackendContextBuilder for BackendContextBuilder {
|
||||||
|
type Context = BackendContext;
|
||||||
|
|
||||||
|
async fn build(self) -> Result<Self::Context> {
|
||||||
|
let mut ctx = BackendContext::default();
|
||||||
|
|
||||||
|
if let Some(maildir) = self.maildir {
|
||||||
|
ctx.maildir = Some(maildir.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(maildir) = self.maildir_for_sync {
|
||||||
|
ctx.maildir_for_sync = Some(maildir.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
if let Some(imap) = self.imap {
|
||||||
|
ctx.imap = Some(imap.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
if let Some(notmuch) = self.notmuch {
|
||||||
|
ctx.notmuch = Some(notmuch.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
if let Some(smtp) = self.smtp {
|
||||||
|
ctx.smtp = Some(smtp.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sendmail) = self.sendmail {
|
||||||
|
ctx.sendmail = Some(sendmail.build().await?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BackendContext {
|
||||||
|
pub maildir: Option<MaildirSessionSync>,
|
||||||
|
pub maildir_for_sync: Option<MaildirSessionSync>,
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
pub imap: Option<ImapSessionSync>,
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
pub smtp: Option<SmtpClientSync>,
|
||||||
|
pub sendmail: Option<SendmailContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BackendBuilder {
|
||||||
|
toml_account_config: TomlAccountConfig,
|
||||||
|
builder: email::backend::BackendBuilder<BackendContextBuilder>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BackendBuilder {
|
||||||
|
pub async fn new(
|
||||||
|
toml_account_config: TomlAccountConfig,
|
||||||
|
account_config: AccountConfig,
|
||||||
|
with_sending: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let used_backends = toml_account_config.get_used_backends();
|
||||||
|
|
||||||
|
let is_maildir_used = used_backends.contains(&BackendKind::Maildir);
|
||||||
|
let is_maildir_for_sync_used = used_backends.contains(&BackendKind::MaildirForSync);
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
let is_imap_used = used_backends.contains(&BackendKind::Imap);
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
let is_notmuch_used = used_backends.contains(&BackendKind::Notmuch);
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
let is_smtp_used = used_backends.contains(&BackendKind::Smtp);
|
||||||
|
let is_sendmail_used = used_backends.contains(&BackendKind::Sendmail);
|
||||||
|
|
||||||
|
let backend_ctx_builder = BackendContextBuilder {
|
||||||
|
maildir: toml_account_config
|
||||||
|
.maildir
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| is_maildir_used)
|
||||||
|
.map(|mdir_config| {
|
||||||
|
MaildirSessionBuilder::new(account_config.clone(), mdir_config.clone())
|
||||||
|
}),
|
||||||
|
maildir_for_sync: Some(MaildirConfig {
|
||||||
|
root_dir: account_config.get_sync_dir()?,
|
||||||
|
})
|
||||||
|
.filter(|_| is_maildir_for_sync_used)
|
||||||
|
.map(|mdir_config| MaildirSessionBuilder::new(account_config.clone(), mdir_config)),
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
imap: {
|
||||||
|
let ctx_builder = toml_account_config
|
||||||
|
.imap
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| is_imap_used)
|
||||||
|
.map(|imap_config| {
|
||||||
|
ImapSessionBuilder::new(account_config.clone(), imap_config.clone())
|
||||||
|
.with_prebuilt_credentials()
|
||||||
|
});
|
||||||
|
|
||||||
|
match ctx_builder {
|
||||||
|
Some(ctx_builder) => Some(ctx_builder.await?),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
notmuch: toml_account_config
|
||||||
|
.notmuch
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| is_notmuch_used)
|
||||||
|
.map(|notmuch_config| {
|
||||||
|
NotmuchSessionBuilder::new(account_config.clone(), notmuch_config.clone())
|
||||||
|
}),
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
smtp: toml_account_config
|
||||||
|
.smtp
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| with_sending)
|
||||||
|
.filter(|_| is_smtp_used)
|
||||||
|
.map(|smtp_config| {
|
||||||
|
SmtpClientBuilder::new(account_config.clone(), smtp_config.clone())
|
||||||
|
}),
|
||||||
|
sendmail: toml_account_config
|
||||||
|
.sendmail
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| with_sending)
|
||||||
|
.filter(|_| is_sendmail_used)
|
||||||
|
.map(|sendmail_config| {
|
||||||
|
SendmailContext::new(account_config.clone(), sendmail_config.clone())
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut backend_builder =
|
||||||
|
email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder);
|
||||||
|
|
||||||
|
match toml_account_config.add_folder_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_folder(|ctx| ctx.maildir.as_ref().and_then(AddFolderMaildir::new));
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_add_folder(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(AddFolderMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_folder(|ctx| ctx.imap.as_ref().and_then(AddFolderImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_folder(|ctx| ctx.notmuch.as_ref().and_then(AddFolderNotmuch::new));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.list_folders_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(ListFoldersMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(ListFoldersMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_list_folders(|ctx| ctx.imap.as_ref().and_then(ListFoldersImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_list_folders(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(ListFoldersNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.expunge_folder_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(ExpungeFolderMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(ExpungeFolderMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_expunge_folder(|ctx| ctx.imap.as_ref().and_then(ExpungeFolderImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_expunge_folder(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(ExpungeFolderNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.purge_folder_kind() {
|
||||||
|
// TODO
|
||||||
|
// Some(BackendKind::Maildir) => {
|
||||||
|
// backend_builder = backend_builder
|
||||||
|
// .with_purge_folder(|ctx| ctx.maildir.as_ref().and_then(PurgeFolderMaildir::new));
|
||||||
|
// }
|
||||||
|
// TODO
|
||||||
|
// Some(BackendKind::MaildirForSync) => {
|
||||||
|
// backend_builder = backend_builder
|
||||||
|
// .with_purge_folder(|ctx| ctx.maildir_for_sync.as_ref().and_then(PurgeFolderMaildir::new));
|
||||||
|
// }
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_purge_folder(|ctx| ctx.imap.as_ref().and_then(PurgeFolderImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_purge_folder(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(PurgeFolderNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.delete_folder_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(DeleteFolderMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(DeleteFolderMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_delete_folder(|ctx| ctx.imap.as_ref().and_then(DeleteFolderImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_delete_folder(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(DeleteFolderNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.get_envelope_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(GetEnvelopeMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(GetEnvelopeMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_get_envelope(|ctx| ctx.imap.as_ref().and_then(GetEnvelopeImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_get_envelope(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(GetEnvelopeNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.list_envelopes_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(ListEnvelopesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(ListEnvelopesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_list_envelopes(|ctx| ctx.imap.as_ref().and_then(ListEnvelopesImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_list_envelopes(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(ListEnvelopesNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.add_flags_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_flags(|ctx| ctx.maildir.as_ref().and_then(AddFlagsMaildir::new));
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_add_flags(|ctx| {
|
||||||
|
ctx.maildir_for_sync.as_ref().and_then(AddFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_flags(|ctx| ctx.imap.as_ref().and_then(AddFlagsImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_flags(|ctx| ctx.notmuch.as_ref().and_then(AddFlagsNotmuch::new));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.set_flags_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_set_flags(|ctx| ctx.maildir.as_ref().and_then(SetFlagsMaildir::new));
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_set_flags(|ctx| {
|
||||||
|
ctx.maildir_for_sync.as_ref().and_then(SetFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_set_flags(|ctx| ctx.imap.as_ref().and_then(SetFlagsImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_set_flags(|ctx| ctx.notmuch.as_ref().and_then(SetFlagsNotmuch::new));
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.remove_flags_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(RemoveFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(RemoveFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_remove_flags(|ctx| ctx.imap.as_ref().and_then(RemoveFlagsImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_remove_flags(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(RemoveFlagsNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.send_raw_message_kind() {
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Some(BackendKind::Smtp) => {
|
||||||
|
backend_builder = backend_builder.with_send_raw_message(|ctx| {
|
||||||
|
ctx.smtp.as_ref().and_then(SendRawMessageSmtp::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::Sendmail) => {
|
||||||
|
backend_builder = backend_builder.with_send_raw_message(|ctx| {
|
||||||
|
ctx.sendmail.as_ref().and_then(SendRawMessageSendmail::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.add_raw_message_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| {
|
||||||
|
ctx.maildir
|
||||||
|
.as_ref()
|
||||||
|
.and_then(AddRawMessageWithFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_add_raw_message_with_flags(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(AddRawMessageWithFlagsMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_add_raw_message(|ctx| ctx.imap.as_ref().and_then(AddRawMessageImap::new))
|
||||||
|
.with_add_raw_message_with_flags(|ctx| {
|
||||||
|
ctx.imap.as_ref().and_then(AddRawMessageWithFlagsImap::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_add_raw_message(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(AddRawMessageNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.peek_messages_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(PeekMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(PeekMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_peek_messages(|ctx| ctx.imap.as_ref().and_then(PeekMessagesImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_peek_messages(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(PeekMessagesNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.get_messages_kind() {
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_get_messages(|ctx| ctx.imap.as_ref().and_then(GetMessagesImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_get_messages(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(GetMessagesNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.copy_messages_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(CopyMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(CopyMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_copy_messages(|ctx| ctx.imap.as_ref().and_then(CopyMessagesImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_copy_messages(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(CopyMessagesNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
match toml_account_config.move_messages_kind() {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||||
|
ctx.maildir.as_ref().and_then(MoveMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||||
|
ctx.maildir_for_sync
|
||||||
|
.as_ref()
|
||||||
|
.and_then(MoveMessagesMaildir::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(BackendKind::Imap) => {
|
||||||
|
backend_builder = backend_builder
|
||||||
|
.with_move_messages(|ctx| ctx.imap.as_ref().and_then(MoveMessagesImap::new));
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
backend_builder = backend_builder.with_move_messages(|ctx| {
|
||||||
|
ctx.notmuch.as_ref().and_then(MoveMessagesNotmuch::new)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
toml_account_config,
|
||||||
|
builder: backend_builder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn build(self) -> Result<Backend> {
|
||||||
|
Ok(Backend {
|
||||||
|
toml_account_config: self.toml_account_config,
|
||||||
|
backend: self.builder.build().await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for BackendBuilder {
|
||||||
|
type Target = email::backend::BackendBuilder<BackendContextBuilder>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.builder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<email::backend::BackendBuilder<BackendContextBuilder>> for BackendBuilder {
|
||||||
|
fn into(self) -> email::backend::BackendBuilder<BackendContextBuilder> {
|
||||||
|
self.builder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Backend {
|
||||||
|
toml_account_config: TomlAccountConfig,
|
||||||
|
backend: email::backend::Backend<BackendContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Backend {
|
||||||
|
pub async fn new(
|
||||||
|
toml_account_config: TomlAccountConfig,
|
||||||
|
account_config: AccountConfig,
|
||||||
|
with_sending: bool,
|
||||||
|
) -> Result<Self> {
|
||||||
|
BackendBuilder::new(toml_account_config, account_config, with_sending)
|
||||||
|
.await?
|
||||||
|
.build()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_id_mapper(
|
||||||
|
&self,
|
||||||
|
folder: &str,
|
||||||
|
backend_kind: Option<&BackendKind>,
|
||||||
|
) -> Result<IdMapper> {
|
||||||
|
let mut id_mapper = IdMapper::Dummy;
|
||||||
|
|
||||||
|
match backend_kind {
|
||||||
|
Some(BackendKind::Maildir) => {
|
||||||
|
if let Some(mdir_config) = &self.toml_account_config.maildir {
|
||||||
|
id_mapper = IdMapper::new(
|
||||||
|
&self.backend.account_config,
|
||||||
|
folder,
|
||||||
|
mdir_config.root_dir.clone(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(BackendKind::MaildirForSync) => {
|
||||||
|
id_mapper = IdMapper::new(
|
||||||
|
&self.backend.account_config,
|
||||||
|
folder,
|
||||||
|
self.backend.account_config.get_sync_dir()?,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(BackendKind::Notmuch) => {
|
||||||
|
if let Some(notmuch_config) = &self.toml_account_config.notmuch {
|
||||||
|
id_mapper = IdMapper::new(
|
||||||
|
&self.backend.account_config,
|
||||||
|
folder,
|
||||||
|
mdir_config.root_dir.clone(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(id_mapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_envelopes(
|
||||||
|
&self,
|
||||||
|
folder: &str,
|
||||||
|
page_size: usize,
|
||||||
|
page: usize,
|
||||||
|
) -> Result<Envelopes> {
|
||||||
|
let backend_kind = self.toml_account_config.list_envelopes_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let envelopes = self.backend.list_envelopes(folder, page_size, page).await?;
|
||||||
|
let envelopes = Envelopes::from_backend(&self.account_config, &id_mapper, envelopes)?;
|
||||||
|
Ok(envelopes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.add_flags(folder, &ids, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.set_flags(folder, &ids, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.remove_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.remove_flags(folder, &ids, flags).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||||
|
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.peek_messages(folder, &ids).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
|
||||||
|
let backend_kind = self.toml_account_config.get_messages_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.get_messages(folder, &ids).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn copy_messages(
|
||||||
|
&self,
|
||||||
|
from_folder: &str,
|
||||||
|
to_folder: &str,
|
||||||
|
ids: &[usize],
|
||||||
|
) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend
|
||||||
|
.copy_messages(from_folder, to_folder, &ids)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn move_messages(
|
||||||
|
&self,
|
||||||
|
from_folder: &str,
|
||||||
|
to_folder: &str,
|
||||||
|
ids: &[usize],
|
||||||
|
) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.move_messages_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend
|
||||||
|
.move_messages(from_folder, to_folder, &ids)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.delete_messages_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.delete_messages(folder, &ids).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_raw_message(&self, folder: &str, email: &[u8]) -> Result<SingleId> {
|
||||||
|
let backend_kind = self.toml_account_config.add_raw_message_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let id = self.backend.add_raw_message(folder, email).await?;
|
||||||
|
id_mapper.create_alias(&*id)?;
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.add_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.add_flag(folder, &ids, flag).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.set_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.set_flag(folder, &ids, flag).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
|
||||||
|
let backend_kind = self.toml_account_config.remove_flags_kind();
|
||||||
|
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
|
||||||
|
let ids = Id::multiple(id_mapper.get_ids(ids)?);
|
||||||
|
self.backend.remove_flag(folder, &ids, flag).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Backend {
|
||||||
|
type Target = email::backend::Backend<BackendContext>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.backend
|
||||||
|
}
|
||||||
|
}
|
71
src/backend/wizard.rs
Normal file
71
src/backend/wizard.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use dialoguer::Select;
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use crate::imap;
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
use crate::notmuch;
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use crate::smtp;
|
||||||
|
use crate::{config::wizard::THEME, maildir, sendmail};
|
||||||
|
|
||||||
|
use super::{config::BackendConfig, BackendKind};
|
||||||
|
|
||||||
|
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
BackendKind::Imap,
|
||||||
|
BackendKind::Maildir,
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
BackendKind::Notmuch,
|
||||||
|
];
|
||||||
|
|
||||||
|
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
BackendKind::Smtp,
|
||||||
|
BackendKind::Sendmail,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<Option<BackendConfig>> {
|
||||||
|
let kind = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("Default email backend")
|
||||||
|
.items(DEFAULT_BACKEND_KINDS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?
|
||||||
|
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
|
||||||
|
|
||||||
|
let config = match kind {
|
||||||
|
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
Some(kind) if kind == BackendKind::Imap => {
|
||||||
|
Some(imap::wizard::configure(account_name, email).await?)
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn configure_sender(
|
||||||
|
account_name: &str,
|
||||||
|
email: &str,
|
||||||
|
) -> Result<Option<BackendConfig>> {
|
||||||
|
let kind = Select::with_theme(&*THEME)
|
||||||
|
.with_prompt("Backend for sending messages")
|
||||||
|
.items(SEND_MESSAGE_BACKEND_KINDS)
|
||||||
|
.default(0)
|
||||||
|
.interact_opt()?
|
||||||
|
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));
|
||||||
|
|
||||||
|
let config = match kind {
|
||||||
|
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
Some(kind) if kind == BackendKind::Smtp => {
|
||||||
|
Some(smtp::wizard::configure(account_name, email).await?)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
15
src/cache/arg/disable.rs
vendored
Normal file
15
src/cache/arg/disable.rs
vendored
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The disable cache flag parser.
|
||||||
|
#[derive(Debug, Default, Parser)]
|
||||||
|
pub struct CacheDisableFlag {
|
||||||
|
/// Disable any sort of cache.
|
||||||
|
///
|
||||||
|
/// The action depends on commands it apply on. For example, when
|
||||||
|
/// listing envelopes using the IMAP backend, this flag will
|
||||||
|
/// ensure that envelopes are fetched from the IMAP server rather
|
||||||
|
/// than the synchronized local Maildir.
|
||||||
|
#[arg(long = "disable-cache", alias = "no-cache", global = true)]
|
||||||
|
#[arg(name = "cache_disable")]
|
||||||
|
pub disable: bool,
|
||||||
|
}
|
1
src/cache/arg/mod.rs
vendored
Normal file
1
src/cache/arg/mod.rs
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod disable;
|
17
src/cache/args.rs
vendored
17
src/cache/args.rs
vendored
|
@ -6,19 +6,24 @@ const ARG_DISABLE_CACHE: &str = "disable-cache";
|
||||||
|
|
||||||
/// Represents the disable cache flag argument. This argument allows
|
/// Represents the disable cache flag argument. This argument allows
|
||||||
/// the user to disable any sort of cache.
|
/// the user to disable any sort of cache.
|
||||||
pub fn arg() -> Arg {
|
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||||
Arg::new(ARG_DISABLE_CACHE)
|
[Arg::new(ARG_DISABLE_CACHE)
|
||||||
.help("Disable any sort of cache")
|
.help("Disable any sort of cache")
|
||||||
.long_help(
|
.long_help(
|
||||||
"Disable any sort of cache. The action depends on
|
"Disable any sort of cache.
|
||||||
the command it applies on.",
|
|
||||||
|
The action depends on commands it apply on. For example, when listing
|
||||||
|
envelopes using the IMAP backend, this flag will ensure that envelopes
|
||||||
|
are fetched from the IMAP server and not from the synchronized local
|
||||||
|
Maildir.",
|
||||||
)
|
)
|
||||||
.long("disable-cache")
|
.long("disable-cache")
|
||||||
|
.alias("no-cache")
|
||||||
.global(true)
|
.global(true)
|
||||||
.action(ArgAction::SetTrue)
|
.action(ArgAction::SetTrue)]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the disable cache flag parser.
|
/// Represents the disable cache flag parser.
|
||||||
pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool {
|
pub fn parse_disable_cache_arg(m: &ArgMatches) -> bool {
|
||||||
m.get_flag(ARG_DISABLE_CACHE)
|
m.get_flag(ARG_DISABLE_CACHE)
|
||||||
}
|
}
|
||||||
|
|
192
src/cache/id_mapper.rs
vendored
192
src/cache/id_mapper.rs
vendored
|
@ -1,192 +0,0 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
use email::backend::ImapBackend;
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
use email::backend::NotmuchBackend;
|
|
||||||
use email::{
|
|
||||||
account::AccountConfig,
|
|
||||||
backend::{Backend, MaildirBackend},
|
|
||||||
};
|
|
||||||
use log::{debug, trace};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum IdMapper {
|
|
||||||
Dummy,
|
|
||||||
Mapper(String, rusqlite::Connection),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IdMapper {
|
|
||||||
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
|
|
||||||
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
|
||||||
let mut db_parent_dir = dir.as_ref().parent();
|
|
||||||
|
|
||||||
while !db_path.is_file() {
|
|
||||||
match db_parent_dir {
|
|
||||||
Some(dir) => {
|
|
||||||
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
|
|
||||||
db_parent_dir = dir.parent();
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db_path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(
|
|
||||||
backend: &dyn Backend,
|
|
||||||
account_config: &AccountConfig,
|
|
||||||
folder: &str,
|
|
||||||
) -> Result<Self> {
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
if backend.as_any().is::<ImapBackend>() {
|
|
||||||
return Ok(IdMapper::Dummy);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut db_path = PathBuf::new();
|
|
||||||
|
|
||||||
if let Some(backend) = backend.as_any().downcast_ref::<MaildirBackend>() {
|
|
||||||
db_path = Self::find_closest_db_path(backend.path())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
if let Some(backend) = backend.as_any().downcast_ref::<NotmuchBackend>() {
|
|
||||||
db_path = Self::find_closest_db_path(backend.path())
|
|
||||||
}
|
|
||||||
|
|
||||||
let folder = account_config.get_folder_alias(folder)?;
|
|
||||||
let digest = md5::compute(account_config.name.clone() + &folder);
|
|
||||||
let table = format!("id_mapper_{digest:x}");
|
|
||||||
debug!("creating id mapper table {table} at {db_path:?}…");
|
|
||||||
|
|
||||||
let conn = rusqlite::Connection::open(&db_path)
|
|
||||||
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
"CREATE TABLE IF NOT EXISTS {table} (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
internal_id TEXT UNIQUE
|
|
||||||
)",
|
|
||||||
);
|
|
||||||
trace!("create table query: {query:#?}");
|
|
||||||
|
|
||||||
conn.execute(&query, [])
|
|
||||||
.context("cannot create id mapper table")?;
|
|
||||||
|
|
||||||
Ok(Self::Mapper(table, conn))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_alias<I>(&self, id: I) -> Result<String>
|
|
||||||
where
|
|
||||||
I: AsRef<str>,
|
|
||||||
{
|
|
||||||
let id = id.as_ref();
|
|
||||||
match self {
|
|
||||||
Self::Dummy => Ok(id.to_owned()),
|
|
||||||
Self::Mapper(table, conn) => {
|
|
||||||
debug!("creating alias for id {id}…");
|
|
||||||
|
|
||||||
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
|
|
||||||
trace!("insert query: {query:#?}");
|
|
||||||
|
|
||||||
conn.execute(&query, [id])
|
|
||||||
.with_context(|| format!("cannot create id alias for id {id}"))?;
|
|
||||||
|
|
||||||
let alias = conn.last_insert_rowid().to_string();
|
|
||||||
debug!("created alias {alias} for id {id}");
|
|
||||||
|
|
||||||
Ok(alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
|
|
||||||
where
|
|
||||||
I: AsRef<str>,
|
|
||||||
{
|
|
||||||
let id = id.as_ref();
|
|
||||||
match self {
|
|
||||||
Self::Dummy => Ok(id.to_owned()),
|
|
||||||
Self::Mapper(table, conn) => {
|
|
||||||
debug!("getting alias for id {id}…");
|
|
||||||
|
|
||||||
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
|
|
||||||
trace!("select query: {query:#?}");
|
|
||||||
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(&query)
|
|
||||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
|
||||||
let aliases: Vec<i64> = stmt
|
|
||||||
.query_map([id], |row| row.get(0))
|
|
||||||
.with_context(|| format!("cannot get alias for id {id}"))?
|
|
||||||
.collect::<rusqlite::Result<_>>()
|
|
||||||
.with_context(|| format!("cannot get alias for id {id}"))?;
|
|
||||||
let alias = match aliases.first() {
|
|
||||||
Some(alias) => {
|
|
||||||
debug!("found alias {alias} for id {id}");
|
|
||||||
alias.to_string()
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
debug!("alias not found, creating it…");
|
|
||||||
self.create_alias(id)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(alias)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id<A>(&self, alias: A) -> Result<String>
|
|
||||||
where
|
|
||||||
A: AsRef<str>,
|
|
||||||
{
|
|
||||||
let alias = alias.as_ref();
|
|
||||||
let alias = alias
|
|
||||||
.parse::<i64>()
|
|
||||||
.context(format!("cannot parse id mapper alias {alias}"))?;
|
|
||||||
|
|
||||||
match self {
|
|
||||||
Self::Dummy => Ok(alias.to_string()),
|
|
||||||
Self::Mapper(table, conn) => {
|
|
||||||
debug!("getting id from alias {alias}…");
|
|
||||||
|
|
||||||
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
|
|
||||||
trace!("select query: {query:#?}");
|
|
||||||
|
|
||||||
let mut stmt = conn
|
|
||||||
.prepare(&query)
|
|
||||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
|
||||||
let ids: Vec<String> = stmt
|
|
||||||
.query_map([alias], |row| row.get(0))
|
|
||||||
.with_context(|| format!("cannot get id from alias {alias}"))?
|
|
||||||
.collect::<rusqlite::Result<_>>()
|
|
||||||
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
|
||||||
let id = ids
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
|
|
||||||
.to_owned();
|
|
||||||
debug!("found id {id} from alias {alias}");
|
|
||||||
|
|
||||||
Ok(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
|
|
||||||
where
|
|
||||||
A: AsRef<str>,
|
|
||||||
I: IntoIterator<Item = A>,
|
|
||||||
{
|
|
||||||
aliases
|
|
||||||
.into_iter()
|
|
||||||
.map(|alias| self.get_id(alias))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
169
src/cache/mod.rs
vendored
169
src/cache/mod.rs
vendored
|
@ -1,4 +1,169 @@
|
||||||
|
pub mod arg;
|
||||||
pub mod args;
|
pub mod args;
|
||||||
mod id_mapper;
|
|
||||||
|
|
||||||
pub use id_mapper::IdMapper;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use email::account::config::AccountConfig;
|
||||||
|
use log::{debug, trace};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum IdMapper {
|
||||||
|
Dummy,
|
||||||
|
Mapper(String, rusqlite::Connection),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IdMapper {
|
||||||
|
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
|
||||||
|
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||||
|
let mut db_parent_dir = dir.as_ref().parent();
|
||||||
|
|
||||||
|
while !db_path.is_file() {
|
||||||
|
match db_parent_dir {
|
||||||
|
Some(dir) => {
|
||||||
|
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
|
||||||
|
db_parent_dir = dir.parent();
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(account_config: &AccountConfig, folder: &str, db_path: PathBuf) -> Result<Self> {
|
||||||
|
let folder = account_config.get_folder_alias(folder)?;
|
||||||
|
let digest = md5::compute(account_config.name.clone() + &folder);
|
||||||
|
let table = format!("id_mapper_{digest:x}");
|
||||||
|
debug!("creating id mapper table {table} at {db_path:?}…");
|
||||||
|
|
||||||
|
let db_path = Self::find_closest_db_path(db_path);
|
||||||
|
let conn = rusqlite::Connection::open(&db_path)
|
||||||
|
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS {table} (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
internal_id TEXT UNIQUE
|
||||||
|
)",
|
||||||
|
);
|
||||||
|
trace!("create table query: {query:#?}");
|
||||||
|
|
||||||
|
conn.execute(&query, [])
|
||||||
|
.context("cannot create id mapper table")?;
|
||||||
|
|
||||||
|
Ok(Self::Mapper(table, conn))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_alias<I>(&self, id: I) -> Result<String>
|
||||||
|
where
|
||||||
|
I: AsRef<str>,
|
||||||
|
{
|
||||||
|
let id = id.as_ref();
|
||||||
|
match self {
|
||||||
|
Self::Dummy => Ok(id.to_owned()),
|
||||||
|
Self::Mapper(table, conn) => {
|
||||||
|
debug!("creating alias for id {id}…");
|
||||||
|
|
||||||
|
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
|
||||||
|
trace!("insert query: {query:#?}");
|
||||||
|
|
||||||
|
conn.execute(&query, [id])
|
||||||
|
.with_context(|| format!("cannot create id alias for id {id}"))?;
|
||||||
|
|
||||||
|
let alias = conn.last_insert_rowid().to_string();
|
||||||
|
debug!("created alias {alias} for id {id}");
|
||||||
|
|
||||||
|
Ok(alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
|
||||||
|
where
|
||||||
|
I: AsRef<str>,
|
||||||
|
{
|
||||||
|
let id = id.as_ref();
|
||||||
|
match self {
|
||||||
|
Self::Dummy => Ok(id.to_owned()),
|
||||||
|
Self::Mapper(table, conn) => {
|
||||||
|
debug!("getting alias for id {id}…");
|
||||||
|
|
||||||
|
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
|
||||||
|
trace!("select query: {query:#?}");
|
||||||
|
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(&query)
|
||||||
|
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||||
|
let aliases: Vec<i64> = stmt
|
||||||
|
.query_map([id], |row| row.get(0))
|
||||||
|
.with_context(|| format!("cannot get alias for id {id}"))?
|
||||||
|
.collect::<rusqlite::Result<_>>()
|
||||||
|
.with_context(|| format!("cannot get alias for id {id}"))?;
|
||||||
|
let alias = match aliases.first() {
|
||||||
|
Some(alias) => {
|
||||||
|
debug!("found alias {alias} for id {id}");
|
||||||
|
alias.to_string()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
debug!("alias not found, creating it…");
|
||||||
|
self.create_alias(id)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_id<A>(&self, alias: A) -> Result<String>
|
||||||
|
where
|
||||||
|
A: ToString,
|
||||||
|
{
|
||||||
|
let alias = alias.to_string();
|
||||||
|
let alias = alias
|
||||||
|
.parse::<i64>()
|
||||||
|
.context(format!("cannot parse id mapper alias {alias}"))?;
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Self::Dummy => Ok(alias.to_string()),
|
||||||
|
Self::Mapper(table, conn) => {
|
||||||
|
debug!("getting id from alias {alias}…");
|
||||||
|
|
||||||
|
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
|
||||||
|
trace!("select query: {query:#?}");
|
||||||
|
|
||||||
|
let mut stmt = conn
|
||||||
|
.prepare(&query)
|
||||||
|
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||||
|
let ids: Vec<String> = stmt
|
||||||
|
.query_map([alias], |row| row.get(0))
|
||||||
|
.with_context(|| format!("cannot get id from alias {alias}"))?
|
||||||
|
.collect::<rusqlite::Result<_>>()
|
||||||
|
.with_context(|| format!("cannot get id from alias {alias}"))?;
|
||||||
|
let id = ids
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
|
||||||
|
.to_owned();
|
||||||
|
debug!("found id {id} from alias {alias}");
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
|
||||||
|
where
|
||||||
|
A: ToString,
|
||||||
|
I: IntoIterator<Item = A>,
|
||||||
|
{
|
||||||
|
aliases
|
||||||
|
.into_iter()
|
||||||
|
.map(|alias| self.get_id(alias))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
132
src/cli.rs
Normal file
132
src/cli.rs
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::command::AccountSubcommand,
|
||||||
|
completion::command::CompletionGenerateCommand,
|
||||||
|
config::{self, TomlConfig},
|
||||||
|
envelope::command::EnvelopeSubcommand,
|
||||||
|
flag::command::FlagSubcommand,
|
||||||
|
folder::command::FolderSubcommand,
|
||||||
|
manual::command::ManualGenerateCommand,
|
||||||
|
message::{
|
||||||
|
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
|
||||||
|
template::command::TemplateSubcommand,
|
||||||
|
},
|
||||||
|
output::{ColorFmt, OutputFmt},
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(name = "himalaya", author, version, about)]
|
||||||
|
#[command(propagate_version = true, infer_subcommands = true)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub command: HimalayaCommand,
|
||||||
|
|
||||||
|
/// Override the default configuration file path
|
||||||
|
///
|
||||||
|
/// The given path is shell-expanded then canonicalized (if
|
||||||
|
/// applicable). If the path does not point to a valid file, the
|
||||||
|
/// wizard will propose to assist you in the creation of the
|
||||||
|
/// configuration file.
|
||||||
|
#[arg(long, short, global = true)]
|
||||||
|
#[arg(value_name = "PATH", value_parser = config::path_parser)]
|
||||||
|
pub config: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Customize the output format
|
||||||
|
///
|
||||||
|
/// The output format determine how to display commands output to
|
||||||
|
/// the terminal.
|
||||||
|
///
|
||||||
|
/// The possible values are:
|
||||||
|
///
|
||||||
|
/// - json: output will be in a form of a JSON-compatible object
|
||||||
|
///
|
||||||
|
/// - plain: output will be in a form of either a plain text or
|
||||||
|
/// table, depending on the command
|
||||||
|
#[arg(long, short, global = true)]
|
||||||
|
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
|
||||||
|
pub output: OutputFmt,
|
||||||
|
|
||||||
|
/// Control when to use colors
|
||||||
|
///
|
||||||
|
/// The default setting is 'auto', which means himalaya will try
|
||||||
|
/// to guess when to use colors. For example, if himalaya is
|
||||||
|
/// printing to a terminal, then it will use colors, but if it is
|
||||||
|
/// redirected to a file or a pipe, then it will suppress color
|
||||||
|
/// output. himalaya will suppress color output in some other
|
||||||
|
/// circumstances as well. For example, if the TERM environment
|
||||||
|
/// variable is not set or set to 'dumb', then himalaya will not
|
||||||
|
/// use colors.
|
||||||
|
///
|
||||||
|
/// The possible values are:
|
||||||
|
///
|
||||||
|
/// - never: colors will never be used
|
||||||
|
///
|
||||||
|
/// - always: colors will always be used regardless of where output is sent
|
||||||
|
///
|
||||||
|
/// - ansi: like 'always', but emits ANSI escapes (even in a Windows console)
|
||||||
|
///
|
||||||
|
/// - auto: himalaya tries to be smart
|
||||||
|
#[arg(long, short = 'C', global = true)]
|
||||||
|
#[arg(value_name = "MODE", value_enum, default_value_t = Default::default())]
|
||||||
|
pub color: ColorFmt,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum HimalayaCommand {
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "accounts")]
|
||||||
|
Account(AccountSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])]
|
||||||
|
#[command(alias = "folders")]
|
||||||
|
Folder(FolderSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "envelopes")]
|
||||||
|
Envelope(EnvelopeSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "flags")]
|
||||||
|
Flag(FlagSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "messages", alias = "msgs", alias = "msg")]
|
||||||
|
Message(MessageSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "attachments")]
|
||||||
|
Attachment(AttachmentSubcommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
#[command(alias = "templates", alias = "tpls", alias = "tpl")]
|
||||||
|
Template(TemplateSubcommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(alias = "manuals", alias = "mans")]
|
||||||
|
Manual(ManualGenerateCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(alias = "completions")]
|
||||||
|
Completion(CompletionGenerateCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HimalayaCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Account(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Folder(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Envelope(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Flag(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Message(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Attachment(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Template(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Manual(cmd) => cmd.execute(printer).await,
|
||||||
|
Self::Completion(cmd) => cmd.execute(printer).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,39 +0,0 @@
|
||||||
//! Module related to completion CLI.
|
|
||||||
//!
|
|
||||||
//! This module provides subcommands and a command matcher related to completion.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
|
||||||
use clap_complete::Shell;
|
|
||||||
use log::debug;
|
|
||||||
|
|
||||||
const ARG_SHELL: &str = "shell";
|
|
||||||
const CMD_COMPLETION: &str = "completion";
|
|
||||||
|
|
||||||
type SomeShell = Shell;
|
|
||||||
|
|
||||||
/// Completion commands.
|
|
||||||
pub enum Cmd {
|
|
||||||
/// Generate completion script for the given shell.
|
|
||||||
Generate(SomeShell),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Completion command matcher.
|
|
||||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|
||||||
if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
|
|
||||||
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
|
|
||||||
debug!("shell: {:?}", shell);
|
|
||||||
return Ok(Some(Cmd::Generate(shell)));
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Completion subcommands.
|
|
||||||
pub fn subcmd() -> Command {
|
|
||||||
Command::new(CMD_COMPLETION)
|
|
||||||
.about("Generates the completion script for the given shell")
|
|
||||||
.args(&[Arg::new(ARG_SHELL)
|
|
||||||
.value_parser(value_parser!(Shell))
|
|
||||||
.required(true)])
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
//! Module related to completion handling.
|
|
||||||
//!
|
|
||||||
//! This module gathers all completion commands.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::Command;
|
|
||||||
use clap_complete::Shell;
|
|
||||||
use std::io::stdout;
|
|
||||||
|
|
||||||
/// Generates completion script from the given [`clap::App`] for the given shell slice.
|
|
||||||
pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
|
|
||||||
let name = cmd.get_name().to_string();
|
|
||||||
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
//! Module related to shell completion.
|
|
||||||
//!
|
|
||||||
//! This module allows users to generate autocompletion scripts for
|
|
||||||
//! their shells. You can see the list of available shells directly on
|
|
||||||
//! the clap's [docs.rs](https://docs.rs/clap/2.33.3/clap/enum.Shell.html).
|
|
||||||
|
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
36
src/completion/command.rs
Normal file
36
src/completion/command.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{value_parser, CommandFactory, Parser};
|
||||||
|
use clap_complete::Shell;
|
||||||
|
use log::info;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crate::{cli::Cli, printer::Printer};
|
||||||
|
|
||||||
|
/// Print completion script for a shell to stdout.
|
||||||
|
///
|
||||||
|
/// This command allows you to generate completion script for a given
|
||||||
|
/// shell. The script is printed to the standard output. If you want
|
||||||
|
/// to write it to a file, just use unix redirection.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct CompletionGenerateCommand {
|
||||||
|
/// Shell for which completion script should be generated for.
|
||||||
|
#[arg(value_parser = value_parser!(Shell))]
|
||||||
|
pub shell: Shell,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CompletionGenerateCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer) -> Result<()> {
|
||||||
|
info!("executing completion generate command");
|
||||||
|
|
||||||
|
let mut cmd = Cli::command();
|
||||||
|
let name = cmd.get_name().to_string();
|
||||||
|
clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout());
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Shell script successfully generated for shell {}!",
|
||||||
|
self.shell
|
||||||
|
))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
1
src/completion/mod.rs
Normal file
1
src/completion/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod command;
|
|
@ -6,16 +6,21 @@ const ARG_CONFIG: &str = "config";
|
||||||
|
|
||||||
/// Represents the config file path argument. This argument allows the
|
/// Represents the config file path argument. This argument allows the
|
||||||
/// user to customize the config file path.
|
/// user to customize the config file path.
|
||||||
pub fn arg() -> Arg {
|
pub fn global_args() -> impl IntoIterator<Item = Arg> {
|
||||||
Arg::new(ARG_CONFIG)
|
[Arg::new(ARG_CONFIG)
|
||||||
.help("Set a custom configuration file path")
|
.help("Override the configuration file path")
|
||||||
|
.long_help(
|
||||||
|
"Override the configuration file path
|
||||||
|
|
||||||
|
If the file under the given path does not exist, the wizard will propose to create it.",
|
||||||
|
)
|
||||||
.long("config")
|
.long("config")
|
||||||
.short('c')
|
.short('c')
|
||||||
.global(true)
|
.global(true)
|
||||||
.value_name("PATH")
|
.value_name("path")]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents the config file path argument parser.
|
/// Represents the config file path argument parser.
|
||||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
pub fn parse_global_arg(matches: &ArgMatches) -> Option<&str> {
|
||||||
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
|
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,652 +0,0 @@
|
||||||
//! Deserialized config module.
|
|
||||||
//!
|
|
||||||
//! This module contains the raw deserialized representation of the
|
|
||||||
//! user configuration file.
|
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use dialoguer::Confirm;
|
|
||||||
use dirs::{config_dir, home_dir};
|
|
||||||
use email::{
|
|
||||||
account::AccountConfig,
|
|
||||||
email::{EmailHooks, EmailTextPlainFormat},
|
|
||||||
};
|
|
||||||
use log::{debug, trace};
|
|
||||||
use process::Cmd;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::HashMap, fs, path::PathBuf, process::exit};
|
|
||||||
use toml;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
account::DeserializedAccountConfig,
|
|
||||||
config::{prelude::*, wizard},
|
|
||||||
wizard_prompt, wizard_warn,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Represents the user config file.
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
|
||||||
#[serde(rename_all = "kebab-case")]
|
|
||||||
pub struct DeserializedConfig {
|
|
||||||
#[serde(alias = "name")]
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub signature_delim: Option<String>,
|
|
||||||
pub signature: Option<String>,
|
|
||||||
pub downloads_dir: Option<PathBuf>,
|
|
||||||
|
|
||||||
pub folder_listing_page_size: Option<usize>,
|
|
||||||
pub folder_aliases: Option<HashMap<String, String>>,
|
|
||||||
|
|
||||||
pub email_listing_page_size: Option<usize>,
|
|
||||||
pub email_listing_datetime_fmt: Option<String>,
|
|
||||||
pub email_listing_datetime_local_tz: Option<bool>,
|
|
||||||
pub email_reading_headers: Option<Vec<String>>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "EmailTextPlainFormatDef",
|
|
||||||
skip_serializing_if = "EmailTextPlainFormat::is_default"
|
|
||||||
)]
|
|
||||||
pub email_reading_format: EmailTextPlainFormat,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_reading_verify_cmd: Option<Cmd>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_reading_decrypt_cmd: Option<Cmd>,
|
|
||||||
pub email_writing_headers: Option<Vec<String>>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_writing_sign_cmd: Option<Cmd>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_writing_encrypt_cmd: Option<Cmd>,
|
|
||||||
pub email_sending_save_copy: Option<bool>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "EmailHooksDef",
|
|
||||||
skip_serializing_if = "EmailHooks::is_empty"
|
|
||||||
)]
|
|
||||||
pub email_hooks: EmailHooks,
|
|
||||||
|
|
||||||
#[serde(flatten)]
|
|
||||||
pub accounts: HashMap<String, DeserializedAccountConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeserializedConfig {
|
|
||||||
/// Tries to create a config from an optional path.
|
|
||||||
pub async fn from_opt_path(path: Option<&str>) -> Result<Self> {
|
|
||||||
debug!("path: {:?}", path);
|
|
||||||
|
|
||||||
let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) {
|
|
||||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
|
||||||
toml::from_str(&content).context("cannot parse config file")?
|
|
||||||
} else {
|
|
||||||
wizard_warn!("Himalaya could not find an already existing configuration file.");
|
|
||||||
|
|
||||||
if !Confirm::new()
|
|
||||||
.with_prompt(wizard_prompt!(
|
|
||||||
"Would you like to create one with the wizard?"
|
|
||||||
))
|
|
||||||
.default(true)
|
|
||||||
.interact_opt()?
|
|
||||||
.unwrap_or_default()
|
|
||||||
{
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
wizard::configure().await?
|
|
||||||
};
|
|
||||||
|
|
||||||
if config.accounts.is_empty() {
|
|
||||||
return Err(anyhow!("config file must contain at least one account"));
|
|
||||||
}
|
|
||||||
|
|
||||||
trace!("config: {:#?}", config);
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tries to return a config path from a few default settings.
|
|
||||||
///
|
|
||||||
/// Tries paths in this order:
|
|
||||||
///
|
|
||||||
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
|
|
||||||
/// OSes.)
|
|
||||||
/// - `"$HOME/.config/himalaya/config.toml"`
|
|
||||||
/// - `"$HOME/.himalayarc"`
|
|
||||||
///
|
|
||||||
/// Returns `Some(path)` if the path exists, otherwise `None`.
|
|
||||||
pub fn path() -> Option<PathBuf> {
|
|
||||||
config_dir()
|
|
||||||
.map(|p| p.join("himalaya").join("config.toml"))
|
|
||||||
.filter(|p| p.exists())
|
|
||||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
|
||||||
.filter(|p| p.exists())
|
|
||||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
|
||||||
.filter(|p| p.exists())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_account_config(&self, account_name: Option<&str>) -> Result<AccountConfig> {
|
|
||||||
let (account_name, deserialized_account_config) = match account_name {
|
|
||||||
Some("default") | Some("") | None => self
|
|
||||||
.accounts
|
|
||||||
.iter()
|
|
||||||
.find_map(|(name, account)| {
|
|
||||||
account
|
|
||||||
.default
|
|
||||||
.filter(|default| *default == true)
|
|
||||||
.map(|_| (name.clone(), account))
|
|
||||||
})
|
|
||||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
|
||||||
Some(name) => self
|
|
||||||
.accounts
|
|
||||||
.get(name)
|
|
||||||
.map(|account| (name.to_string(), account))
|
|
||||||
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(deserialized_account_config.to_account_config(account_name, self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use email::{
|
|
||||||
account::PasswdConfig,
|
|
||||||
backend::{BackendConfig, MaildirConfig},
|
|
||||||
sender::{SenderConfig, SendmailConfig},
|
|
||||||
};
|
|
||||||
use secret::Secret;
|
|
||||||
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
use email::backend::NotmuchConfig;
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
use email::backend::{ImapAuthConfig, ImapConfig};
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
use email::sender::{SmtpAuthConfig, SmtpConfig};
|
|
||||||
|
|
||||||
use std::io::Write;
|
|
||||||
use tempfile::NamedTempFile;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
async fn make_config(config: &str) -> Result<DeserializedConfig> {
|
|
||||||
let mut file = NamedTempFile::new().unwrap();
|
|
||||||
write!(file, "{}", config).unwrap();
|
|
||||||
DeserializedConfig::from_opt_path(file.into_temp_path().to_str()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn empty_config() {
|
|
||||||
let config = make_config("").await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap_err().root_cause().to_string(),
|
|
||||||
"config file must contain at least one account"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_missing_email_field() {
|
|
||||||
let config = make_config("[account]").await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `email`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_missing_backend_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `backend`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_invalid_backend_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"bad\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("unknown variant `bad`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn imap_account_missing_host_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"imap\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `imap-host`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_imap_missing_port_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"imap\"
|
|
||||||
imap-host = \"localhost\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `imap-port`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_imap_missing_login_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"imap\"
|
|
||||||
imap-host = \"localhost\"
|
|
||||||
imap-port = 993",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `imap-login`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_imap_missing_passwd_cmd_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"imap\"
|
|
||||||
imap-host = \"localhost\"
|
|
||||||
imap-port = 993
|
|
||||||
imap-login = \"login\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `imap-auth`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_maildir_missing_root_dir_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"maildir\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `maildir-root-dir`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_notmuch_missing_db_path_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"notmuch\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `notmuch-db-path`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_missing_sender_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `sender`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_invalid_sender_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"bad\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_smtp_sender_missing_host_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"smtp\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `smtp-host`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_smtp_sender_missing_port_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"smtp\"
|
|
||||||
smtp-host = \"localhost\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `smtp-port`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_smtp_sender_missing_login_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"smtp\"
|
|
||||||
smtp-host = \"localhost\"
|
|
||||||
smtp-port = 25",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `smtp-login`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_smtp_sender_missing_auth_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"smtp\"
|
|
||||||
smtp-host = \"localhost\"
|
|
||||||
smtp-port = 25
|
|
||||||
smtp-login = \"login\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(config
|
|
||||||
.unwrap_err()
|
|
||||||
.root_cause()
|
|
||||||
.to_string()
|
|
||||||
.contains("missing field `smtp-auth`"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_sendmail_sender_missing_cmd_field() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"sendmail\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
|
||||||
cmd: "/usr/sbin/sendmail".into()
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_smtp_sender_minimum_config() {
|
|
||||||
use email::sender::SenderConfig;
|
|
||||||
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"smtp\"
|
|
||||||
smtp-host = \"localhost\"
|
|
||||||
smtp-port = 25
|
|
||||||
smtp-login = \"login\"
|
|
||||||
smtp-auth = \"passwd\"
|
|
||||||
smtp-passwd = { cmd = \"echo password\" }",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
sender: SenderConfig::Smtp(SmtpConfig {
|
|
||||||
host: "localhost".into(),
|
|
||||||
port: 25,
|
|
||||||
login: "login".into(),
|
|
||||||
auth: SmtpAuthConfig::Passwd(PasswdConfig {
|
|
||||||
passwd: Secret::new_cmd(String::from("echo password"))
|
|
||||||
}),
|
|
||||||
..SmtpConfig::default()
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_sendmail_sender_minimum_config() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
backend = \"none\"
|
|
||||||
sender = \"sendmail\"
|
|
||||||
sendmail-cmd = \"echo send\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
sender: SenderConfig::Sendmail(SendmailConfig {
|
|
||||||
cmd: Cmd::from("echo send")
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_imap_minimum_config() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"imap\"
|
|
||||||
imap-host = \"localhost\"
|
|
||||||
imap-port = 993
|
|
||||||
imap-login = \"login\"
|
|
||||||
imap-auth = \"passwd\"
|
|
||||||
imap-passwd = { cmd = \"echo password\" }",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
backend: BackendConfig::Imap(ImapConfig {
|
|
||||||
host: "localhost".into(),
|
|
||||||
port: 993,
|
|
||||||
login: "login".into(),
|
|
||||||
auth: ImapAuthConfig::Passwd(PasswdConfig {
|
|
||||||
passwd: Secret::new_cmd(String::from("echo password"))
|
|
||||||
}),
|
|
||||||
..ImapConfig::default()
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_maildir_minimum_config() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"maildir\"
|
|
||||||
maildir-root-dir = \"/tmp/maildir\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
backend: BackendConfig::Maildir(MaildirConfig {
|
|
||||||
root_dir: "/tmp/maildir".into(),
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
#[tokio::test]
|
|
||||||
async fn account_backend_notmuch_minimum_config() {
|
|
||||||
let config = make_config(
|
|
||||||
"[account]
|
|
||||||
email = \"test@localhost\"
|
|
||||||
sender = \"none\"
|
|
||||||
backend = \"notmuch\"
|
|
||||||
notmuch-db-path = \"/tmp/notmuch.db\"",
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
config.unwrap(),
|
|
||||||
DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
email: "test@localhost".into(),
|
|
||||||
backend: BackendConfig::Notmuch(NotmuchConfig {
|
|
||||||
db_path: "/tmp/notmuch.db".into(),
|
|
||||||
}),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
}
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,729 @@
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod config;
|
|
||||||
pub mod prelude;
|
|
||||||
pub mod wizard;
|
pub mod wizard;
|
||||||
|
|
||||||
pub use config::*;
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use dialoguer::Confirm;
|
||||||
|
use dirs::{config_dir, home_dir};
|
||||||
|
use email::{
|
||||||
|
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
|
||||||
|
folder::config::FolderConfig, message::config::MessageConfig,
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use shellexpand_utils::{canonicalize, expand};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process,
|
||||||
|
};
|
||||||
|
use toml;
|
||||||
|
|
||||||
|
use crate::{account::config::TomlAccountConfig, backend::BackendKind, wizard_prompt, wizard_warn};
|
||||||
|
|
||||||
|
/// Represents the user config file.
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub struct TomlConfig {
|
||||||
|
#[serde(alias = "name")]
|
||||||
|
pub display_name: Option<String>,
|
||||||
|
pub signature: Option<String>,
|
||||||
|
pub signature_delim: Option<String>,
|
||||||
|
pub downloads_dir: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub accounts: HashMap<String, TomlAccountConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TomlConfig {
|
||||||
|
/// Read and parse the TOML configuration at the given path.
|
||||||
|
///
|
||||||
|
/// Returns an error if the configuration file cannot be read or
|
||||||
|
/// if its content cannot be parsed.
|
||||||
|
fn from_path(path: &Path) -> Result<Self> {
|
||||||
|
let content =
|
||||||
|
fs::read_to_string(path).context(format!("cannot read config file at {path:?}"))?;
|
||||||
|
toml::from_str(&content).context(format!("cannot parse config file at {path:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create and save a TOML configuration using the wizard.
|
||||||
|
///
|
||||||
|
/// If the user accepts the confirmation, the wizard starts and
|
||||||
|
/// help him to create his configuration file. Otherwise the
|
||||||
|
/// program stops.
|
||||||
|
///
|
||||||
|
/// NOTE: the wizard can only be used with interactive shells.
|
||||||
|
async fn from_wizard(path: PathBuf) -> Result<Self> {
|
||||||
|
wizard_warn!("Cannot find existing configuration at {path:?}.");
|
||||||
|
|
||||||
|
let confirm = Confirm::new()
|
||||||
|
.with_prompt(wizard_prompt!(
|
||||||
|
"Would you like to create one with the wizard?"
|
||||||
|
))
|
||||||
|
.default(true)
|
||||||
|
.interact_opt()?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !confirm {
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard::configure(path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read and parse the TOML configuration from default paths.
|
||||||
|
pub async fn from_default_paths() -> Result<Self> {
|
||||||
|
match Self::first_valid_default_path() {
|
||||||
|
Some(path) => Self::from_path(&path),
|
||||||
|
None => Self::from_wizard(Self::default_path()?).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read and parse the TOML configuration at the optional given
|
||||||
|
/// path.
|
||||||
|
///
|
||||||
|
/// If the given path exists, then read and parse the TOML
|
||||||
|
/// configuration from it.
|
||||||
|
///
|
||||||
|
/// If the given path does not exist, then create it using the
|
||||||
|
/// wizard.
|
||||||
|
///
|
||||||
|
/// If no path is given, then either read and parse the TOML
|
||||||
|
/// configuration at the first valid default path, otherwise
|
||||||
|
/// create it using the wizard. wizard.
|
||||||
|
pub async fn from_some_path_or_default(path: Option<impl Into<PathBuf>>) -> Result<Self> {
|
||||||
|
match path.map(Into::into) {
|
||||||
|
Some(ref path) if path.exists() => Self::from_path(path),
|
||||||
|
Some(path) => Self::from_wizard(path).await,
|
||||||
|
None => Self::from_default_paths().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the default configuration path.
|
||||||
|
///
|
||||||
|
/// Returns an error if the XDG configuration directory cannot be
|
||||||
|
/// found.
|
||||||
|
pub fn default_path() -> Result<PathBuf> {
|
||||||
|
Ok(config_dir()
|
||||||
|
.ok_or(anyhow!("cannot get XDG config directory"))?
|
||||||
|
.join("himalaya")
|
||||||
|
.join("config.toml"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first default configuration path that points to a
|
||||||
|
/// valid file.
|
||||||
|
///
|
||||||
|
/// Tries paths in this order:
|
||||||
|
///
|
||||||
|
/// - `$XDG_CONFIG_DIR/himalaya/config.toml` (or equivalent to
|
||||||
|
/// `$XDG_CONFIG_DIR` in other OSes.)
|
||||||
|
/// - `$HOME/.config/himalaya/config.toml`
|
||||||
|
/// - `$HOME/.himalayarc`
|
||||||
|
pub fn first_valid_default_path() -> Option<PathBuf> {
|
||||||
|
Self::default_path()
|
||||||
|
.ok()
|
||||||
|
.filter(|p| p.exists())
|
||||||
|
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||||
|
.filter(|p| p.exists())
|
||||||
|
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||||
|
.filter(|p| p.exists())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_toml_account_config(
|
||||||
|
&self,
|
||||||
|
account_name: Option<&str>,
|
||||||
|
) -> Result<(String, TomlAccountConfig)> {
|
||||||
|
let (account_name, mut toml_account_config) = match account_name {
|
||||||
|
Some("default") | Some("") | None => self
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.find_map(|(name, account)| {
|
||||||
|
account
|
||||||
|
.default
|
||||||
|
.filter(|default| *default == true)
|
||||||
|
.map(|_| (name.to_owned(), account.clone()))
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||||
|
Some(name) => self
|
||||||
|
.accounts
|
||||||
|
.get(name)
|
||||||
|
.map(|account| (name.to_owned(), account.clone()))
|
||||||
|
.ok_or_else(|| anyhow!("cannot find account {name}")),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
if let Some(imap_config) = toml_account_config.imap.as_mut() {
|
||||||
|
imap_config
|
||||||
|
.auth
|
||||||
|
.replace_undefined_keyring_entries(&account_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
|
||||||
|
smtp_config
|
||||||
|
.auth
|
||||||
|
.replace_undefined_keyring_entries(&account_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((account_name, toml_account_config))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build account configurations from a given account name.
|
||||||
|
pub fn into_account_configs(
|
||||||
|
self,
|
||||||
|
account_name: Option<&str>,
|
||||||
|
disable_cache: bool,
|
||||||
|
) -> Result<(TomlAccountConfig, AccountConfig)> {
|
||||||
|
let (account_name, mut toml_account_config) =
|
||||||
|
self.into_toml_account_config(account_name)?;
|
||||||
|
|
||||||
|
if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) {
|
||||||
|
if !disable_cache {
|
||||||
|
toml_account_config.backend = Some(BackendKind::MaildirForSync);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
display_name: self.display_name,
|
||||||
|
signature: self.signature,
|
||||||
|
signature_delim: self.signature_delim,
|
||||||
|
downloads_dir: self.downloads_dir,
|
||||||
|
|
||||||
|
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|
||||||
|
|(name, config)| {
|
||||||
|
(
|
||||||
|
name.clone(),
|
||||||
|
AccountConfig {
|
||||||
|
name,
|
||||||
|
email: config.email,
|
||||||
|
display_name: config.display_name,
|
||||||
|
signature: config.signature,
|
||||||
|
signature_delim: config.signature_delim,
|
||||||
|
downloads_dir: config.downloads_dir,
|
||||||
|
|
||||||
|
folder: config.folder.map(|c| FolderConfig {
|
||||||
|
aliases: c.alias,
|
||||||
|
list: c.list.map(|c| c.remote),
|
||||||
|
}),
|
||||||
|
envelope: config.envelope.map(|c| EnvelopeConfig {
|
||||||
|
list: c.list.map(|c| c.remote),
|
||||||
|
}),
|
||||||
|
message: config.message.map(|c| MessageConfig {
|
||||||
|
read: c.read.map(|c| c.remote),
|
||||||
|
write: c.write.map(|c| c.remote),
|
||||||
|
send: c.send.map(|c| c.remote),
|
||||||
|
}),
|
||||||
|
sync: config.sync,
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
pgp: config.pgp,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let account_config = config.account(&account_name)?;
|
||||||
|
|
||||||
|
Ok((toml_account_config, account_config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a configuration file path as [`PathBuf`].
|
||||||
|
///
|
||||||
|
/// The path is shell-expanded then canonicalized (if applicable).
|
||||||
|
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
|
||||||
|
expand::try_path(path)
|
||||||
|
.map(canonicalize::path)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use email::{
|
||||||
|
account::config::passwd::PasswdConfig, maildir::config::MaildirConfig,
|
||||||
|
sendmail::config::SendmailConfig,
|
||||||
|
};
|
||||||
|
use secret::Secret;
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
use email::backend::NotmuchConfig;
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
use email::imap::config::{ImapAuthConfig, ImapConfig};
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
use email::smtp::config::{SmtpAuthConfig, SmtpConfig};
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
async fn make_config(config: &str) -> Result<TomlConfig> {
|
||||||
|
let mut file = NamedTempFile::new().unwrap();
|
||||||
|
write!(file, "{}", config).unwrap();
|
||||||
|
TomlConfig::from_some_path_or_default(file.into_temp_path().to_str()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn empty_config() {
|
||||||
|
let config = make_config("").await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap_err().root_cause().to_string(),
|
||||||
|
"config file must contain at least one account"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_missing_email_field() {
|
||||||
|
let config = make_config("[account]").await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `email`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_missing_backend_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `backend`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_invalid_backend_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"bad\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("unknown variant `bad`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn imap_account_missing_host_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"imap\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `imap-host`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_imap_missing_port_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"imap\"
|
||||||
|
imap-host = \"localhost\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `imap-port`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_imap_missing_login_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"imap\"
|
||||||
|
imap-host = \"localhost\"
|
||||||
|
imap-port = 993",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `imap-login`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_imap_missing_passwd_cmd_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"imap\"
|
||||||
|
imap-host = \"localhost\"
|
||||||
|
imap-port = 993
|
||||||
|
imap-login = \"login\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `imap-auth`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_maildir_missing_root_dir_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"maildir\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `maildir-root-dir`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_notmuch_missing_db_path_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"notmuch\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `notmuch-db-path`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_missing_sender_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `sender`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_invalid_sender_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"bad\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_smtp_sender_missing_host_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"smtp\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `smtp-host`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_smtp_sender_missing_port_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"smtp\"
|
||||||
|
smtp-host = \"localhost\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `smtp-port`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_smtp_sender_missing_login_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"smtp\"
|
||||||
|
smtp-host = \"localhost\"
|
||||||
|
smtp-port = 25",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `smtp-login`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_smtp_sender_missing_auth_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"smtp\"
|
||||||
|
smtp-host = \"localhost\"
|
||||||
|
smtp-port = 25
|
||||||
|
smtp-login = \"login\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(config
|
||||||
|
.unwrap_err()
|
||||||
|
.root_cause()
|
||||||
|
.to_string()
|
||||||
|
.contains("missing field `smtp-auth`"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_sendmail_sender_missing_cmd_field() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"sendmail\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||||
|
cmd: "/usr/sbin/sendmail".into()
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_smtp_sender_minimum_config() {
|
||||||
|
use email::sender::SenderConfig;
|
||||||
|
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"smtp\"
|
||||||
|
smtp-host = \"localhost\"
|
||||||
|
smtp-port = 25
|
||||||
|
smtp-login = \"login\"
|
||||||
|
smtp-auth = \"passwd\"
|
||||||
|
smtp-passwd = { cmd = \"echo password\" }",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
sender: SenderConfig::Smtp(SmtpConfig {
|
||||||
|
host: "localhost".into(),
|
||||||
|
port: 25,
|
||||||
|
login: "login".into(),
|
||||||
|
auth: SmtpAuthConfig::Passwd(PasswdConfig {
|
||||||
|
passwd: Secret::new_cmd(String::from("echo password"))
|
||||||
|
}),
|
||||||
|
..SmtpConfig::default()
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_sendmail_sender_minimum_config() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
backend = \"none\"
|
||||||
|
sender = \"sendmail\"
|
||||||
|
sendmail-cmd = \"echo send\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
sender: SenderConfig::Sendmail(SendmailConfig {
|
||||||
|
cmd: Cmd::from("echo send")
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_imap_minimum_config() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"imap\"
|
||||||
|
imap-host = \"localhost\"
|
||||||
|
imap-port = 993
|
||||||
|
imap-login = \"login\"
|
||||||
|
imap-auth = \"passwd\"
|
||||||
|
imap-passwd = { cmd = \"echo password\" }",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
backend: BackendConfig::Imap(ImapConfig {
|
||||||
|
host: "localhost".into(),
|
||||||
|
port: 993,
|
||||||
|
login: "login".into(),
|
||||||
|
auth: ImapAuthConfig::Passwd(PasswdConfig {
|
||||||
|
passwd: Secret::new_cmd(String::from("echo password"))
|
||||||
|
}),
|
||||||
|
..ImapConfig::default()
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_maildir_minimum_config() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"maildir\"
|
||||||
|
maildir-root-dir = \"/tmp/maildir\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
backend: BackendConfig::Maildir(MaildirConfig {
|
||||||
|
root_dir: "/tmp/maildir".into(),
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn account_backend_notmuch_minimum_config() {
|
||||||
|
let config = make_config(
|
||||||
|
"[account]
|
||||||
|
email = \"test@localhost\"
|
||||||
|
sender = \"none\"
|
||||||
|
backend = \"notmuch\"
|
||||||
|
notmuch-db-path = \"/tmp/notmuch.db\"",
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
config.unwrap(),
|
||||||
|
TomlConfig {
|
||||||
|
accounts: HashMap::from_iter([(
|
||||||
|
"account".into(),
|
||||||
|
TomlAccountConfig {
|
||||||
|
email: "test@localhost".into(),
|
||||||
|
backend: BackendConfig::Notmuch(NotmuchConfig {
|
||||||
|
db_path: "/tmp/notmuch.db".into(),
|
||||||
|
}),
|
||||||
|
..TomlAccountConfig::default()
|
||||||
|
}
|
||||||
|
)]),
|
||||||
|
..TomlConfig::default()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,18 +6,21 @@ use email::account::GpgConfig;
|
||||||
use email::account::PgpConfig;
|
use email::account::PgpConfig;
|
||||||
#[cfg(feature = "pgp-native")]
|
#[cfg(feature = "pgp-native")]
|
||||||
use email::account::{NativePgpConfig, NativePgpSecretKey, SignedSecretKey};
|
use email::account::{NativePgpConfig, NativePgpSecretKey, SignedSecretKey};
|
||||||
#[cfg(feature = "notmuch-backend")]
|
#[cfg(feature = "notmuch")]
|
||||||
use email::backend::NotmuchConfig;
|
use email::backend::NotmuchConfig;
|
||||||
#[cfg(feature = "imap-backend")]
|
#[cfg(feature = "imap")]
|
||||||
use email::backend::{ImapAuthConfig, ImapConfig};
|
use email::imap::config::{ImapAuthConfig, ImapConfig};
|
||||||
#[cfg(feature = "smtp-sender")]
|
#[cfg(feature = "smtp")]
|
||||||
use email::sender::{SmtpAuthConfig, SmtpConfig};
|
use email::smtp::config::{SmtpAuthConfig, SmtpConfig};
|
||||||
use email::{
|
use email::{
|
||||||
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
|
account::config::{
|
||||||
backend::{BackendConfig, MaildirConfig},
|
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
|
||||||
email::{EmailHooks, EmailTextPlainFormat},
|
passwd::PasswdConfig,
|
||||||
|
},
|
||||||
|
email::config::{EmailHooks, EmailTextPlainFormat},
|
||||||
folder::sync::FolderSyncStrategy,
|
folder::sync::FolderSyncStrategy,
|
||||||
sender::{SenderConfig, SendmailConfig},
|
maildir::config::MaildirConfig,
|
||||||
|
sendmail::config::SendmailConfig,
|
||||||
};
|
};
|
||||||
use keyring::Entry;
|
use keyring::Entry;
|
||||||
use process::{Cmd, Pipeline, SingleCmd};
|
use process::{Cmd, Pipeline, SingleCmd};
|
||||||
|
@ -61,27 +64,38 @@ pub enum CmdDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "Option<Cmd>", from = "OptionCmd")]
|
#[serde(remote = "Option<Cmd>", from = "OptionCmd", into = "OptionCmd")]
|
||||||
pub struct OptionCmdDef;
|
pub struct OptionCmdDef;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(untagged)]
|
pub struct OptionCmd {
|
||||||
pub enum OptionCmd {
|
#[serde(default, skip)]
|
||||||
#[default]
|
is_some: bool,
|
||||||
#[serde(skip_serializing)]
|
#[serde(flatten, with = "CmdDef")]
|
||||||
None,
|
inner: Cmd,
|
||||||
#[serde(with = "SingleCmdDef")]
|
|
||||||
SingleCmd(SingleCmd),
|
|
||||||
#[serde(with = "PipelineDef")]
|
|
||||||
Pipeline(Pipeline),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<OptionCmd> for Option<Cmd> {
|
impl From<OptionCmd> for Option<Cmd> {
|
||||||
fn from(cmd: OptionCmd) -> Option<Cmd> {
|
fn from(cmd: OptionCmd) -> Option<Cmd> {
|
||||||
match cmd {
|
if cmd.is_some {
|
||||||
OptionCmd::None => None,
|
Some(cmd.inner)
|
||||||
OptionCmd::SingleCmd(cmd) => Some(Cmd::SingleCmd(cmd)),
|
} else {
|
||||||
OptionCmd::Pipeline(pipeline) => Some(Cmd::Pipeline(pipeline)),
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionCmd> for Option<Cmd> {
|
||||||
|
fn into(self) -> OptionCmd {
|
||||||
|
match self {
|
||||||
|
Some(cmd) => OptionCmd {
|
||||||
|
is_some: true,
|
||||||
|
inner: cmd,
|
||||||
|
},
|
||||||
|
None => OptionCmd {
|
||||||
|
is_some: false,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -108,49 +122,66 @@ pub enum OAuth2MethodDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "BackendConfig", tag = "backend", rename_all = "kebab-case")]
|
#[serde(
|
||||||
pub enum BackendConfigDef {
|
remote = "Option<ImapConfig>",
|
||||||
#[default]
|
from = "OptionImapConfig",
|
||||||
None,
|
into = "OptionImapConfig"
|
||||||
#[cfg(feature = "imap-backend")]
|
)]
|
||||||
#[serde(with = "ImapConfigDef")]
|
pub struct OptionImapConfigDef;
|
||||||
Imap(ImapConfig),
|
|
||||||
#[serde(with = "MaildirConfigDef")]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
Maildir(MaildirConfig),
|
pub struct OptionImapConfig {
|
||||||
#[cfg(feature = "notmuch-backend")]
|
#[serde(default, skip)]
|
||||||
#[serde(with = "NotmuchConfigDef")]
|
is_none: bool,
|
||||||
Notmuch(NotmuchConfig),
|
#[serde(flatten, with = "ImapConfigDef")]
|
||||||
|
inner: ImapConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
impl From<OptionImapConfig> for Option<ImapConfig> {
|
||||||
|
fn from(config: OptionImapConfig) -> Option<ImapConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionImapConfig> for Option<ImapConfig> {
|
||||||
|
fn into(self) -> OptionImapConfig {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionImapConfig {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionImapConfig {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "ImapConfig")]
|
#[serde(remote = "ImapConfig", rename_all = "kebab-case")]
|
||||||
pub struct ImapConfigDef {
|
pub struct ImapConfigDef {
|
||||||
#[serde(rename = "imap-host")]
|
|
||||||
pub host: String,
|
pub host: String,
|
||||||
#[serde(rename = "imap-port")]
|
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
#[serde(rename = "imap-ssl")]
|
|
||||||
pub ssl: Option<bool>,
|
pub ssl: Option<bool>,
|
||||||
#[serde(rename = "imap-starttls")]
|
|
||||||
pub starttls: Option<bool>,
|
pub starttls: Option<bool>,
|
||||||
#[serde(rename = "imap-insecure")]
|
|
||||||
pub insecure: Option<bool>,
|
pub insecure: Option<bool>,
|
||||||
#[serde(rename = "imap-login")]
|
|
||||||
pub login: String,
|
pub login: String,
|
||||||
#[serde(flatten, with = "ImapAuthConfigDef")]
|
#[serde(flatten, with = "ImapAuthConfigDef")]
|
||||||
pub auth: ImapAuthConfig,
|
pub auth: ImapAuthConfig,
|
||||||
#[serde(rename = "imap-notify-cmd")]
|
|
||||||
pub notify_cmd: Option<String>,
|
pub notify_cmd: Option<String>,
|
||||||
#[serde(rename = "imap-notify-query")]
|
|
||||||
pub notify_query: Option<String>,
|
pub notify_query: Option<String>,
|
||||||
#[serde(rename = "imap-watch-cmds")]
|
|
||||||
pub watch_cmds: Option<Vec<String>>,
|
pub watch_cmds: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
#[cfg(feature = "imap")]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "ImapAuthConfig", tag = "imap-auth")]
|
#[serde(remote = "ImapAuthConfig", tag = "auth")]
|
||||||
pub enum ImapAuthConfigDef {
|
pub enum ImapAuthConfigDef {
|
||||||
#[serde(rename = "passwd", alias = "password", with = "ImapPasswdConfigDef")]
|
#[serde(rename = "passwd", alias = "password", with = "ImapPasswdConfigDef")]
|
||||||
Passwd(#[serde(default)] PasswdConfig),
|
Passwd(#[serde(default)] PasswdConfig),
|
||||||
|
@ -162,7 +193,7 @@ pub enum ImapAuthConfigDef {
|
||||||
#[serde(remote = "PasswdConfig")]
|
#[serde(remote = "PasswdConfig")]
|
||||||
pub struct ImapPasswdConfigDef {
|
pub struct ImapPasswdConfigDef {
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "imap-passwd",
|
rename = "passwd",
|
||||||
with = "SecretDef",
|
with = "SecretDef",
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Secret::is_undefined"
|
skip_serializing_if = "Secret::is_undefined"
|
||||||
|
@ -227,6 +258,47 @@ pub enum ImapOAuth2ScopesDef {
|
||||||
Scopes(Vec<String>),
|
Scopes(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<MaildirConfig>",
|
||||||
|
from = "OptionMaildirConfig",
|
||||||
|
into = "OptionMaildirConfig"
|
||||||
|
)]
|
||||||
|
pub struct OptionMaildirConfigDef;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionMaildirConfig {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "MaildirConfigDef")]
|
||||||
|
inner: MaildirConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OptionMaildirConfig> for Option<MaildirConfig> {
|
||||||
|
fn from(config: OptionMaildirConfig) -> Option<MaildirConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionMaildirConfig> for Option<MaildirConfig> {
|
||||||
|
fn into(self) -> OptionMaildirConfig {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionMaildirConfig {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionMaildirConfig {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "MaildirConfig", rename_all = "kebab-case")]
|
#[serde(remote = "MaildirConfig", rename_all = "kebab-case")]
|
||||||
pub struct MaildirConfigDef {
|
pub struct MaildirConfigDef {
|
||||||
|
@ -234,7 +306,52 @@ pub struct MaildirConfigDef {
|
||||||
pub root_dir: PathBuf,
|
pub root_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
#[cfg(feature = "notmuch")]
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<NotmuchConfig>",
|
||||||
|
from = "OptionNotmuchConfig",
|
||||||
|
into = "OptionNotmuchConfig"
|
||||||
|
)]
|
||||||
|
pub struct OptionNotmuchConfigDef;
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionNotmuchConfig {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "NotmuchConfigDef")]
|
||||||
|
inner: NotmuchConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
impl From<OptionNotmuchConfig> for Option<NotmuchConfig> {
|
||||||
|
fn from(config: OptionNotmuchConfig) -> Option<NotmuchConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
impl Into<OptionNotmuchConfig> for Option<NotmuchConfig> {
|
||||||
|
fn into(self) -> OptionNotmuchConfig {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionNotmuchConfig {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionNotmuchConfig {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "NotmuchConfig", rename_all = "kebab-case")]
|
#[serde(remote = "NotmuchConfig", rename_all = "kebab-case")]
|
||||||
pub struct NotmuchConfigDef {
|
pub struct NotmuchConfigDef {
|
||||||
|
@ -242,6 +359,47 @@ pub struct NotmuchConfigDef {
|
||||||
pub db_path: PathBuf,
|
pub db_path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<EmailTextPlainFormat>",
|
||||||
|
from = "OptionEmailTextPlainFormat",
|
||||||
|
into = "OptionEmailTextPlainFormat"
|
||||||
|
)]
|
||||||
|
pub struct OptionEmailTextPlainFormatDef;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionEmailTextPlainFormat {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "EmailTextPlainFormatDef")]
|
||||||
|
inner: EmailTextPlainFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OptionEmailTextPlainFormat> for Option<EmailTextPlainFormat> {
|
||||||
|
fn from(fmt: OptionEmailTextPlainFormat) -> Option<EmailTextPlainFormat> {
|
||||||
|
if fmt.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(fmt.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionEmailTextPlainFormat> for Option<EmailTextPlainFormat> {
|
||||||
|
fn into(self) -> OptionEmailTextPlainFormat {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionEmailTextPlainFormat {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionEmailTextPlainFormat {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(
|
#[serde(
|
||||||
remote = "EmailTextPlainFormat",
|
remote = "EmailTextPlainFormat",
|
||||||
|
@ -257,40 +415,63 @@ pub enum EmailTextPlainFormatDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "SenderConfig", tag = "sender", rename_all = "kebab-case")]
|
#[serde(
|
||||||
pub enum SenderConfigDef {
|
remote = "Option<SmtpConfig>",
|
||||||
#[default]
|
from = "OptionSmtpConfig",
|
||||||
None,
|
into = "OptionSmtpConfig"
|
||||||
#[cfg(feature = "smtp-sender")]
|
)]
|
||||||
#[serde(with = "SmtpConfigDef")]
|
pub struct OptionSmtpConfigDef;
|
||||||
Smtp(SmtpConfig),
|
|
||||||
#[serde(with = "SendmailConfigDef")]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
Sendmail(SendmailConfig),
|
pub struct OptionSmtpConfig {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "SmtpConfigDef")]
|
||||||
|
inner: SmtpConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
impl From<OptionSmtpConfig> for Option<SmtpConfig> {
|
||||||
|
fn from(config: OptionSmtpConfig) -> Option<SmtpConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionSmtpConfig> for Option<SmtpConfig> {
|
||||||
|
fn into(self) -> OptionSmtpConfig {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionSmtpConfig {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionSmtpConfig {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "SmtpConfig")]
|
#[serde(remote = "SmtpConfig")]
|
||||||
struct SmtpConfigDef {
|
struct SmtpConfigDef {
|
||||||
#[serde(rename = "smtp-host")]
|
|
||||||
pub host: String,
|
pub host: String,
|
||||||
#[serde(rename = "smtp-port")]
|
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
#[serde(rename = "smtp-ssl")]
|
|
||||||
pub ssl: Option<bool>,
|
pub ssl: Option<bool>,
|
||||||
#[serde(rename = "smtp-starttls")]
|
|
||||||
pub starttls: Option<bool>,
|
pub starttls: Option<bool>,
|
||||||
#[serde(rename = "smtp-insecure")]
|
|
||||||
pub insecure: Option<bool>,
|
pub insecure: Option<bool>,
|
||||||
#[serde(rename = "smtp-login")]
|
|
||||||
pub login: String,
|
pub login: String,
|
||||||
#[serde(flatten, with = "SmtpAuthConfigDef")]
|
#[serde(flatten, with = "SmtpAuthConfigDef")]
|
||||||
pub auth: SmtpAuthConfig,
|
pub auth: SmtpAuthConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
#[cfg(feature = "smtp")]
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "SmtpAuthConfig", tag = "smtp-auth")]
|
#[serde(remote = "SmtpAuthConfig", tag = "auth")]
|
||||||
pub enum SmtpAuthConfigDef {
|
pub enum SmtpAuthConfigDef {
|
||||||
#[serde(rename = "passwd", alias = "password", with = "SmtpPasswdConfigDef")]
|
#[serde(rename = "passwd", alias = "password", with = "SmtpPasswdConfigDef")]
|
||||||
Passwd(#[serde(default)] PasswdConfig),
|
Passwd(#[serde(default)] PasswdConfig),
|
||||||
|
@ -302,7 +483,7 @@ pub enum SmtpAuthConfigDef {
|
||||||
#[serde(remote = "PasswdConfig", default)]
|
#[serde(remote = "PasswdConfig", default)]
|
||||||
pub struct SmtpPasswdConfigDef {
|
pub struct SmtpPasswdConfigDef {
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "smtp-passwd",
|
rename = "passwd",
|
||||||
with = "SecretDef",
|
with = "SecretDef",
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Secret::is_undefined"
|
skip_serializing_if = "Secret::is_undefined"
|
||||||
|
@ -311,32 +492,26 @@ pub struct SmtpPasswdConfigDef {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "OAuth2Config")]
|
#[serde(remote = "OAuth2Config", rename_all = "kebab-case")]
|
||||||
pub struct SmtpOAuth2ConfigDef {
|
pub struct SmtpOAuth2ConfigDef {
|
||||||
#[serde(rename = "smtp-oauth2-method", with = "OAuth2MethodDef", default)]
|
#[serde(with = "OAuth2MethodDef", default)]
|
||||||
pub method: OAuth2Method,
|
pub method: OAuth2Method,
|
||||||
#[serde(rename = "smtp-oauth2-client-id")]
|
|
||||||
pub client_id: String,
|
pub client_id: String,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "smtp-oauth2-client-secret",
|
|
||||||
with = "SecretDef",
|
with = "SecretDef",
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Secret::is_undefined"
|
skip_serializing_if = "Secret::is_undefined"
|
||||||
)]
|
)]
|
||||||
pub client_secret: Secret,
|
pub client_secret: Secret,
|
||||||
#[serde(rename = "smtp-oauth2-auth-url")]
|
|
||||||
pub auth_url: String,
|
pub auth_url: String,
|
||||||
#[serde(rename = "smtp-oauth2-token-url")]
|
|
||||||
pub token_url: String,
|
pub token_url: String,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "smtp-oauth2-access-token",
|
|
||||||
with = "SecretDef",
|
with = "SecretDef",
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Secret::is_undefined"
|
skip_serializing_if = "Secret::is_undefined"
|
||||||
)]
|
)]
|
||||||
pub access_token: Secret,
|
pub access_token: Secret,
|
||||||
#[serde(
|
#[serde(
|
||||||
rename = "smtp-oauth2-refresh-token",
|
|
||||||
with = "SecretDef",
|
with = "SecretDef",
|
||||||
default,
|
default,
|
||||||
skip_serializing_if = "Secret::is_undefined"
|
skip_serializing_if = "Secret::is_undefined"
|
||||||
|
@ -344,17 +519,11 @@ pub struct SmtpOAuth2ConfigDef {
|
||||||
pub refresh_token: Secret,
|
pub refresh_token: Secret,
|
||||||
#[serde(flatten, with = "SmtpOAuth2ScopesDef")]
|
#[serde(flatten, with = "SmtpOAuth2ScopesDef")]
|
||||||
pub scopes: OAuth2Scopes,
|
pub scopes: OAuth2Scopes,
|
||||||
#[serde(rename = "smtp-oauth2-pkce", default)]
|
#[serde(default)]
|
||||||
pub pkce: bool,
|
pub pkce: bool,
|
||||||
#[serde(
|
#[serde(default = "OAuth2Config::default_redirect_host")]
|
||||||
rename = "imap-oauth2-redirect-host",
|
|
||||||
default = "OAuth2Config::default_redirect_host"
|
|
||||||
)]
|
|
||||||
pub redirect_host: String,
|
pub redirect_host: String,
|
||||||
#[serde(
|
#[serde(default = "OAuth2Config::default_redirect_port")]
|
||||||
rename = "imap-oauth2-redirect-port",
|
|
||||||
default = "OAuth2Config::default_redirect_port"
|
|
||||||
)]
|
|
||||||
pub redirect_port: u16,
|
pub redirect_port: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -367,14 +536,51 @@ pub enum SmtpOAuth2ScopesDef {
|
||||||
Scopes(Vec<String>),
|
Scopes(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<SendmailConfig>",
|
||||||
|
from = "OptionSendmailConfig",
|
||||||
|
into = "OptionSendmailConfig"
|
||||||
|
)]
|
||||||
|
pub struct OptionSendmailConfigDef;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionSendmailConfig {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "SendmailConfigDef")]
|
||||||
|
inner: SendmailConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OptionSendmailConfig> for Option<SendmailConfig> {
|
||||||
|
fn from(config: OptionSendmailConfig) -> Option<SendmailConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionSendmailConfig> for Option<SendmailConfig> {
|
||||||
|
fn into(self) -> OptionSendmailConfig {
|
||||||
|
match self {
|
||||||
|
Some(config) => OptionSendmailConfig {
|
||||||
|
is_none: false,
|
||||||
|
inner: config,
|
||||||
|
},
|
||||||
|
None => OptionSendmailConfig {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
|
#[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
|
||||||
pub struct SendmailConfigDef {
|
pub struct SendmailConfigDef {
|
||||||
#[serde(
|
#[serde(with = "CmdDef", default = "sendmail_default_cmd")]
|
||||||
rename = "sendmail-cmd",
|
|
||||||
with = "CmdDef",
|
|
||||||
default = "sendmail_default_cmd"
|
|
||||||
)]
|
|
||||||
cmd: Cmd,
|
cmd: Cmd,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,6 +588,51 @@ fn sendmail_default_cmd() -> Cmd {
|
||||||
Cmd::from("/usr/sbin/sendmail")
|
Cmd::from("/usr/sbin/sendmail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<EmailHooks>",
|
||||||
|
from = "OptionEmailHooks",
|
||||||
|
into = "OptionEmailHooks"
|
||||||
|
)]
|
||||||
|
pub struct OptionEmailHooksDef;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionEmailHooks {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(
|
||||||
|
flatten,
|
||||||
|
skip_serializing_if = "EmailHooks::is_empty",
|
||||||
|
with = "EmailHooksDef"
|
||||||
|
)]
|
||||||
|
inner: EmailHooks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OptionEmailHooks> for Option<EmailHooks> {
|
||||||
|
fn from(hooks: OptionEmailHooks) -> Option<EmailHooks> {
|
||||||
|
if hooks.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(hooks.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionEmailHooks> for Option<EmailHooks> {
|
||||||
|
fn into(self) -> OptionEmailHooks {
|
||||||
|
match self {
|
||||||
|
Some(hooks) => OptionEmailHooks {
|
||||||
|
is_none: false,
|
||||||
|
inner: hooks,
|
||||||
|
},
|
||||||
|
None => OptionEmailHooks {
|
||||||
|
is_none: true,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the email hooks. Useful for doing extra email
|
/// Represents the email hooks. Useful for doing extra email
|
||||||
/// processing before or after sending it.
|
/// processing before or after sending it.
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -392,6 +643,51 @@ pub struct EmailHooksDef {
|
||||||
pub pre_send: Option<Cmd>,
|
pub pre_send: Option<Cmd>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(
|
||||||
|
remote = "Option<FolderSyncStrategy>",
|
||||||
|
from = "OptionFolderSyncStrategy",
|
||||||
|
into = "OptionFolderSyncStrategy"
|
||||||
|
)]
|
||||||
|
pub struct OptionFolderSyncStrategyDef;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionFolderSyncStrategy {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_some: bool,
|
||||||
|
#[serde(
|
||||||
|
flatten,
|
||||||
|
skip_serializing_if = "FolderSyncStrategy::is_default",
|
||||||
|
with = "FolderSyncStrategyDef"
|
||||||
|
)]
|
||||||
|
inner: FolderSyncStrategy,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OptionFolderSyncStrategy> for Option<FolderSyncStrategy> {
|
||||||
|
fn from(option: OptionFolderSyncStrategy) -> Option<FolderSyncStrategy> {
|
||||||
|
if option.is_some {
|
||||||
|
Some(option.inner)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<OptionFolderSyncStrategy> for Option<FolderSyncStrategy> {
|
||||||
|
fn into(self) -> OptionFolderSyncStrategy {
|
||||||
|
match self {
|
||||||
|
Some(strategy) => OptionFolderSyncStrategy {
|
||||||
|
is_some: true,
|
||||||
|
inner: strategy,
|
||||||
|
},
|
||||||
|
None => OptionFolderSyncStrategy {
|
||||||
|
is_some: false,
|
||||||
|
inner: Default::default(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "FolderSyncStrategy", rename_all = "kebab-case")]
|
#[serde(remote = "FolderSyncStrategy", rename_all = "kebab-case")]
|
||||||
pub enum FolderSyncStrategyDef {
|
pub enum FolderSyncStrategyDef {
|
||||||
|
@ -406,10 +702,33 @@ pub enum FolderSyncStrategyDef {
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
#[cfg(feature = "pgp")]
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
#[serde(remote = "Option<PgpConfig>", from = "OptionPgpConfig")]
|
||||||
|
pub struct OptionPgpConfigDef;
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct OptionPgpConfig {
|
||||||
|
#[serde(default, skip)]
|
||||||
|
is_none: bool,
|
||||||
|
#[serde(flatten, with = "PgpConfigDef")]
|
||||||
|
inner: PgpConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
impl From<OptionPgpConfig> for Option<PgpConfig> {
|
||||||
|
fn from(config: OptionPgpConfig) -> Option<PgpConfig> {
|
||||||
|
if config.is_none {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(config.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(remote = "PgpConfig", tag = "backend", rename_all = "kebab-case")]
|
#[serde(remote = "PgpConfig", tag = "backend", rename_all = "kebab-case")]
|
||||||
pub enum PgpConfigDef {
|
pub enum PgpConfigDef {
|
||||||
#[default]
|
|
||||||
None,
|
|
||||||
#[cfg(feature = "pgp-commands")]
|
#[cfg(feature = "pgp-commands")]
|
||||||
#[serde(with = "CmdsPgpConfigDef", alias = "commands")]
|
#[serde(with = "CmdsPgpConfigDef", alias = "commands")]
|
||||||
Cmds(CmdsPgpConfig),
|
Cmds(CmdsPgpConfig),
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
use super::DeserializedConfig;
|
|
||||||
use crate::account;
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
|
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use shellexpand_utils::{shellexpand_path, try_shellexpand_path};
|
use shellexpand_utils::expand;
|
||||||
use std::{env, fs, io, process};
|
use std::{fs, io, path::PathBuf, process};
|
||||||
|
use toml_edit::{Document, Item};
|
||||||
|
|
||||||
|
use crate::account;
|
||||||
|
|
||||||
|
use super::TomlConfig;
|
||||||
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! wizard_warn {
|
macro_rules! wizard_warn {
|
||||||
|
@ -31,10 +34,10 @@ macro_rules! wizard_log {
|
||||||
|
|
||||||
pub(crate) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
|
pub(crate) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
|
||||||
|
|
||||||
pub(crate) async fn configure() -> Result<DeserializedConfig> {
|
pub(crate) async fn configure(path: PathBuf) -> Result<TomlConfig> {
|
||||||
wizard_log!("Configuring your first account:");
|
wizard_log!("Configuring your first account:");
|
||||||
|
|
||||||
let mut config = DeserializedConfig::default();
|
let mut config = TomlConfig::default();
|
||||||
|
|
||||||
while let Some((name, account_config)) = account::wizard::configure().await? {
|
while let Some((name, account_config)) = account::wizard::configure().await? {
|
||||||
config.accounts.insert(name, account_config);
|
config.accounts.insert(name, account_config);
|
||||||
|
@ -57,7 +60,10 @@ pub(crate) async fn configure() -> Result<DeserializedConfig> {
|
||||||
// accounts are setup, decide which will be the default. If no
|
// accounts are setup, decide which will be the default. If no
|
||||||
// accounts are setup, exit the process.
|
// accounts are setup, exit the process.
|
||||||
let default_account = match config.accounts.len() {
|
let default_account = match config.accounts.len() {
|
||||||
0 => process::exit(0),
|
0 => {
|
||||||
|
wizard_warn!("No account configured, exiting.");
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
1 => Some(config.accounts.values_mut().next().unwrap()),
|
1 => Some(config.accounts.values_mut().next().unwrap()),
|
||||||
_ => {
|
_ => {
|
||||||
let accounts = config.accounts.clone();
|
let accounts = config.accounts.clone();
|
||||||
|
@ -86,25 +92,86 @@ pub(crate) async fn configure() -> Result<DeserializedConfig> {
|
||||||
.with_prompt(wizard_prompt!(
|
.with_prompt(wizard_prompt!(
|
||||||
"Where would you like to save your configuration?"
|
"Where would you like to save your configuration?"
|
||||||
))
|
))
|
||||||
.default(
|
.default(path.to_string_lossy().to_string())
|
||||||
dirs::config_dir()
|
|
||||||
.map(|p| p.join("himalaya").join("config.toml"))
|
|
||||||
.unwrap_or_else(|| env::temp_dir().join("himalaya").join("config.toml"))
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string(),
|
|
||||||
)
|
|
||||||
.validate_with(|path: &String| try_shellexpand_path(path).map(|_| ()))
|
|
||||||
.interact()?;
|
.interact()?;
|
||||||
let path = shellexpand_path(&path);
|
let path = expand::path(&path);
|
||||||
|
|
||||||
println!("Writing the configuration to {path:?}…");
|
println!("Writing the configuration to {path:?}…");
|
||||||
|
|
||||||
|
let mut doc = toml::to_string(&config)?.parse::<Document>()?;
|
||||||
|
|
||||||
|
doc.iter_mut().for_each(|(_, item)| {
|
||||||
|
set_table_dotted(item, "folder-aliases");
|
||||||
|
set_table_dotted(item, "sync-folders-strategy");
|
||||||
|
|
||||||
|
set_table_dotted(item, "folder");
|
||||||
|
get_table_mut(item, "folder").map(|item| {
|
||||||
|
set_tables_dotted(item, ["add", "list", "expunge", "purge", "delete"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_table_dotted(item, "envelope");
|
||||||
|
get_table_mut(item, "envelope").map(|item| {
|
||||||
|
set_tables_dotted(item, ["list", "get"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_table_dotted(item, "flag");
|
||||||
|
get_table_mut(item, "flag").map(|item| {
|
||||||
|
set_tables_dotted(item, ["add", "set", "remove"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_table_dotted(item, "message");
|
||||||
|
get_table_mut(item, "message").map(|item| {
|
||||||
|
set_tables_dotted(
|
||||||
|
item,
|
||||||
|
["add", "send", "peek", "get", "copy", "move", "delete"],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_table_dotted(item, "maildir");
|
||||||
|
#[cfg(feature = "imap")]
|
||||||
|
{
|
||||||
|
set_table_dotted(item, "imap");
|
||||||
|
get_table_mut(item, "imap").map(|item| {
|
||||||
|
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#[cfg(feature = "notmuch")]
|
||||||
|
set_table_dotted(item, "notmuch");
|
||||||
|
set_table_dotted(item, "sendmail");
|
||||||
|
#[cfg(feature = "smtp")]
|
||||||
|
{
|
||||||
|
set_table_dotted(item, "smtp");
|
||||||
|
get_table_mut(item, "smtp").map(|item| {
|
||||||
|
set_tables_dotted(item, ["passwd", "oauth2"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "pgp")]
|
||||||
|
set_table_dotted(item, "pgp");
|
||||||
|
});
|
||||||
|
|
||||||
fs::create_dir_all(path.parent().unwrap_or(&path))?;
|
fs::create_dir_all(path.parent().unwrap_or(&path))?;
|
||||||
fs::write(path, toml::to_string(&config)?)?;
|
fs::write(path, doc.to_string())?;
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> {
|
||||||
|
item.get_mut(key).filter(|item| item.is_table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_table_dotted(item: &mut Item, key: &str) {
|
||||||
|
get_table_mut(item, key)
|
||||||
|
.and_then(|item| item.as_table_mut())
|
||||||
|
.map(|table| table.set_dotted(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a str>) {
|
||||||
|
for key in keys {
|
||||||
|
set_table_dotted(item, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn prompt_passwd(prompt: &str) -> io::Result<String> {
|
pub(crate) fn prompt_passwd(prompt: &str) -> io::Result<String> {
|
||||||
Password::with_theme(&*THEME)
|
Password::with_theme(&*THEME)
|
||||||
.with_prompt(prompt)
|
.with_prompt(prompt)
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
//! Account module.
|
|
||||||
//!
|
|
||||||
//! This module contains the definition of the printable account,
|
|
||||||
//! which is only used by the "accounts" command to list all available
|
|
||||||
//! accounts from the config file.
|
|
||||||
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use crate::ui::table::{Cell, Row, Table};
|
|
||||||
|
|
||||||
/// Represents the printable account.
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
|
||||||
pub struct Account {
|
|
||||||
/// Represents the account name.
|
|
||||||
pub name: String,
|
|
||||||
/// Represents the backend name of the account.
|
|
||||||
pub backend: String,
|
|
||||||
/// Represents the default state of the account.
|
|
||||||
pub default: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Account {
|
|
||||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.into(),
|
|
||||||
backend: backend.into(),
|
|
||||||
default,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Account {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Table for Account {
|
|
||||||
fn head() -> Row {
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
|
||||||
.cell(Cell::new("BACKEND").bold().underline().white())
|
|
||||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row(&self) -> Row {
|
|
||||||
let default = if self.default { "yes" } else { "" };
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new(&self.name).shrinkable().green())
|
|
||||||
.cell(Cell::new(&self.backend).blue())
|
|
||||||
.cell(Cell::new(default).white())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
//! Account module.
|
|
||||||
//!
|
|
||||||
//! This module contains the definition of the printable account,
|
|
||||||
//! which is only used by the "accounts" command to list all available
|
|
||||||
//! accounts from the config file.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use email::backend::BackendConfig;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::{collections::hash_map::Iter, ops::Deref};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
|
||||||
ui::Table,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{Account, DeserializedAccountConfig};
|
|
||||||
|
|
||||||
/// Represents the list of printable accounts.
|
|
||||||
#[derive(Debug, Default, Serialize)]
|
|
||||||
pub struct Accounts(pub Vec<Account>);
|
|
||||||
|
|
||||||
impl Deref for Accounts {
|
|
||||||
type Target = Vec<Account>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrintTable for Accounts {
|
|
||||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
|
||||||
writeln!(writer)?;
|
|
||||||
Table::print(writer, self, opts)?;
|
|
||||||
writeln!(writer)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
|
|
||||||
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
|
|
||||||
let mut accounts: Vec<_> = map
|
|
||||||
.map(|(name, account)| match &account.backend {
|
|
||||||
BackendConfig::None => Account::new(name, "none", false),
|
|
||||||
BackendConfig::Maildir(_) => {
|
|
||||||
Account::new(name, "maildir", account.default.unwrap_or_default())
|
|
||||||
}
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
BackendConfig::Imap(_) => {
|
|
||||||
Account::new(name, "imap", account.default.unwrap_or_default())
|
|
||||||
}
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
BackendConfig::Notmuch(_) => {
|
|
||||||
Account::new(name, "notmuch", account.default.unwrap_or_default())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
|
||||||
Self(accounts)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
//! This module provides arguments related to the user account config.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
||||||
use email::folder::sync::FolderSyncStrategy;
|
|
||||||
use log::info;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use crate::{folder, ui::table};
|
|
||||||
|
|
||||||
const ARG_ACCOUNT: &str = "account";
|
|
||||||
const ARG_DRY_RUN: &str = "dry-run";
|
|
||||||
const ARG_RESET: &str = "reset";
|
|
||||||
const CMD_ACCOUNTS: &str = "accounts";
|
|
||||||
const CMD_CONFIGURE: &str = "configure";
|
|
||||||
const CMD_LIST: &str = "list";
|
|
||||||
const CMD_SYNC: &str = "sync";
|
|
||||||
|
|
||||||
type DryRun = bool;
|
|
||||||
type Reset = bool;
|
|
||||||
|
|
||||||
/// Represents the account commands.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Cmd {
|
|
||||||
/// Represents the list accounts command.
|
|
||||||
List(table::args::MaxTableWidth),
|
|
||||||
/// Represents the sync account command.
|
|
||||||
Sync(Option<FolderSyncStrategy>, DryRun),
|
|
||||||
/// Configure the current selected account.
|
|
||||||
Configure(Reset),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the account command matcher.
|
|
||||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|
||||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
|
|
||||||
if let Some(m) = m.subcommand_matches(CMD_SYNC) {
|
|
||||||
info!("sync account subcommand matched");
|
|
||||||
let dry_run = parse_dry_run_arg(m);
|
|
||||||
let include = folder::args::parse_include_arg(m);
|
|
||||||
let exclude = folder::args::parse_exclude_arg(m);
|
|
||||||
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) {
|
|
||||||
Some(FolderSyncStrategy::Include(HashSet::from_iter([
|
|
||||||
folder.to_owned()
|
|
||||||
])))
|
|
||||||
} else if !include.is_empty() {
|
|
||||||
Some(FolderSyncStrategy::Include(include.to_owned()))
|
|
||||||
} else if !exclude.is_empty() {
|
|
||||||
Some(FolderSyncStrategy::Exclude(exclude))
|
|
||||||
} else if folder::args::parse_all_arg(m) {
|
|
||||||
Some(FolderSyncStrategy::All)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Some(Cmd::Sync(folders_strategy, dry_run))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
|
||||||
info!("list accounts subcommand matched");
|
|
||||||
let max_table_width = table::args::parse_max_width(m);
|
|
||||||
Some(Cmd::List(max_table_width))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
|
|
||||||
info!("configure account subcommand matched");
|
|
||||||
let reset = parse_reset_flag(m);
|
|
||||||
Some(Cmd::Configure(reset))
|
|
||||||
} else {
|
|
||||||
info!("no account subcommand matched, falling back to subcommand list");
|
|
||||||
Some(Cmd::List(None))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the account subcommand.
|
|
||||||
pub fn subcmd() -> Command {
|
|
||||||
Command::new(CMD_ACCOUNTS)
|
|
||||||
.about("Manage accounts")
|
|
||||||
.subcommands([
|
|
||||||
Command::new(CMD_LIST)
|
|
||||||
.about("List all accounts from the config file")
|
|
||||||
.arg(table::args::max_width()),
|
|
||||||
Command::new(CMD_SYNC)
|
|
||||||
.about("Synchronize the given account locally")
|
|
||||||
.arg(folder::args::all_arg("Synchronize all folders"))
|
|
||||||
.arg(folder::args::include_arg(
|
|
||||||
"Synchronize only the given folders",
|
|
||||||
))
|
|
||||||
.arg(folder::args::exclude_arg(
|
|
||||||
"Synchronize all folders except the given ones",
|
|
||||||
))
|
|
||||||
.arg(dry_run()),
|
|
||||||
Command::new(CMD_CONFIGURE)
|
|
||||||
.about("Configure the current selected account")
|
|
||||||
.aliases(["config", "conf", "cfg"])
|
|
||||||
.arg(reset_flag()),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the user account name argument. This argument allows
|
|
||||||
/// the user to select a different account than the default one.
|
|
||||||
pub fn arg() -> Arg {
|
|
||||||
Arg::new(ARG_ACCOUNT)
|
|
||||||
.help("Set the account")
|
|
||||||
.long("account")
|
|
||||||
.short('a')
|
|
||||||
.global(true)
|
|
||||||
.value_name("STRING")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the user account name argument parser.
|
|
||||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
|
||||||
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the user account sync dry run flag. This flag allows
|
|
||||||
/// the user to see the changes of a sync without applying them.
|
|
||||||
pub fn dry_run() -> Arg {
|
|
||||||
Arg::new(ARG_DRY_RUN)
|
|
||||||
.help("Do not apply changes of the synchronization")
|
|
||||||
.long_help(
|
|
||||||
"Do not apply changes of the synchronization.
|
|
||||||
Changes can be visualized with the RUST_LOG=trace environment variable.",
|
|
||||||
)
|
|
||||||
.short('d')
|
|
||||||
.long("dry-run")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the user account sync dry run flag parser.
|
|
||||||
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
|
|
||||||
m.get_flag(ARG_DRY_RUN)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reset_flag() -> Arg {
|
|
||||||
Arg::new(ARG_RESET)
|
|
||||||
.help("Reset the configuration")
|
|
||||||
.short('r')
|
|
||||||
.long("reset")
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_reset_flag(m: &ArgMatches) -> bool {
|
|
||||||
m.get_flag(ARG_RESET)
|
|
||||||
}
|
|
|
@ -1,232 +0,0 @@
|
||||||
//! Deserialized account config module.
|
|
||||||
//!
|
|
||||||
//! This module contains the raw deserialized representation of an
|
|
||||||
//! account in the accounts section of the user configuration file.
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
use email::account::PgpConfig;
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
use email::backend::ImapAuthConfig;
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
use email::sender::SmtpAuthConfig;
|
|
||||||
use email::{
|
|
||||||
account::AccountConfig,
|
|
||||||
backend::BackendConfig,
|
|
||||||
email::{EmailHooks, EmailTextPlainFormat},
|
|
||||||
folder::sync::FolderSyncStrategy,
|
|
||||||
sender::SenderConfig,
|
|
||||||
};
|
|
||||||
|
|
||||||
use process::Cmd;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
|
||||||
|
|
||||||
use crate::config::{prelude::*, DeserializedConfig};
|
|
||||||
|
|
||||||
/// Represents all existing kind of account config.
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
|
||||||
#[serde(tag = "backend", rename_all = "kebab-case")]
|
|
||||||
pub struct DeserializedAccountConfig {
|
|
||||||
pub email: String,
|
|
||||||
pub default: Option<bool>,
|
|
||||||
pub display_name: Option<String>,
|
|
||||||
pub signature_delim: Option<String>,
|
|
||||||
pub signature: Option<String>,
|
|
||||||
pub downloads_dir: Option<PathBuf>,
|
|
||||||
|
|
||||||
pub folder_listing_page_size: Option<usize>,
|
|
||||||
pub folder_aliases: Option<HashMap<String, String>>,
|
|
||||||
|
|
||||||
pub email_listing_page_size: Option<usize>,
|
|
||||||
pub email_listing_datetime_fmt: Option<String>,
|
|
||||||
pub email_listing_datetime_local_tz: Option<bool>,
|
|
||||||
pub email_reading_headers: Option<Vec<String>>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "EmailTextPlainFormatDef",
|
|
||||||
skip_serializing_if = "EmailTextPlainFormat::is_default"
|
|
||||||
)]
|
|
||||||
pub email_reading_format: EmailTextPlainFormat,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_reading_verify_cmd: Option<Cmd>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_reading_decrypt_cmd: Option<Cmd>,
|
|
||||||
pub email_writing_headers: Option<Vec<String>>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_writing_sign_cmd: Option<Cmd>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "OptionCmdDef",
|
|
||||||
skip_serializing_if = "Option::is_none"
|
|
||||||
)]
|
|
||||||
pub email_writing_encrypt_cmd: Option<Cmd>,
|
|
||||||
pub email_sending_save_copy: Option<bool>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "EmailHooksDef",
|
|
||||||
skip_serializing_if = "EmailHooks::is_empty"
|
|
||||||
)]
|
|
||||||
pub email_hooks: EmailHooks,
|
|
||||||
|
|
||||||
pub sync: Option<bool>,
|
|
||||||
pub sync_dir: Option<PathBuf>,
|
|
||||||
#[serde(
|
|
||||||
default,
|
|
||||||
with = "FolderSyncStrategyDef",
|
|
||||||
skip_serializing_if = "FolderSyncStrategy::is_default"
|
|
||||||
)]
|
|
||||||
pub sync_folders_strategy: FolderSyncStrategy,
|
|
||||||
|
|
||||||
#[serde(flatten, with = "BackendConfigDef")]
|
|
||||||
pub backend: BackendConfig,
|
|
||||||
#[serde(flatten, with = "SenderConfigDef")]
|
|
||||||
pub sender: SenderConfig,
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
#[serde(default, with = "PgpConfigDef")]
|
|
||||||
pub pgp: PgpConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DeserializedAccountConfig {
|
|
||||||
pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig {
|
|
||||||
let mut folder_aliases = config
|
|
||||||
.folder_aliases
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_default();
|
|
||||||
folder_aliases.extend(
|
|
||||||
self.folder_aliases
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
AccountConfig {
|
|
||||||
name: name.clone(),
|
|
||||||
email: self.email.to_owned(),
|
|
||||||
display_name: self
|
|
||||||
.display_name
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)),
|
|
||||||
signature_delim: self
|
|
||||||
.signature_delim
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)),
|
|
||||||
signature: self
|
|
||||||
.signature
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.signature.as_ref().map(ToOwned::to_owned)),
|
|
||||||
downloads_dir: self
|
|
||||||
.downloads_dir
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)),
|
|
||||||
folder_listing_page_size: self
|
|
||||||
.folder_listing_page_size
|
|
||||||
.or_else(|| config.folder_listing_page_size),
|
|
||||||
folder_aliases,
|
|
||||||
email_listing_page_size: self
|
|
||||||
.email_listing_page_size
|
|
||||||
.or_else(|| config.email_listing_page_size),
|
|
||||||
email_listing_datetime_fmt: self
|
|
||||||
.email_listing_datetime_fmt
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| {
|
|
||||||
config
|
|
||||||
.email_listing_datetime_fmt
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
}),
|
|
||||||
email_listing_datetime_local_tz: self
|
|
||||||
.email_listing_datetime_local_tz
|
|
||||||
.or_else(|| config.email_listing_datetime_local_tz),
|
|
||||||
email_reading_headers: self
|
|
||||||
.email_reading_headers
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)),
|
|
||||||
email_reading_format: self.email_reading_format.clone(),
|
|
||||||
email_writing_headers: self
|
|
||||||
.email_writing_headers
|
|
||||||
.as_ref()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)),
|
|
||||||
email_sending_save_copy: self.email_sending_save_copy.unwrap_or(true),
|
|
||||||
email_hooks: EmailHooks {
|
|
||||||
pre_send: self.email_hooks.pre_send.clone(),
|
|
||||||
},
|
|
||||||
sync: self.sync.unwrap_or_default(),
|
|
||||||
sync_dir: self.sync_dir.clone(),
|
|
||||||
sync_folders_strategy: self.sync_folders_strategy.clone(),
|
|
||||||
|
|
||||||
backend: {
|
|
||||||
let mut backend = self.backend.clone();
|
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
if let BackendConfig::Imap(config) = &mut backend {
|
|
||||||
match &mut config.auth {
|
|
||||||
ImapAuthConfig::Passwd(secret) => {
|
|
||||||
secret.set_keyring_entry_if_undefined(format!("{name}-imap-passwd"));
|
|
||||||
}
|
|
||||||
ImapAuthConfig::OAuth2(config) => {
|
|
||||||
config.client_secret.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-imap-oauth2-client-secret"
|
|
||||||
));
|
|
||||||
config.access_token.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-imap-oauth2-access-token"
|
|
||||||
));
|
|
||||||
config.refresh_token.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-imap-oauth2-refresh-token"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
backend
|
|
||||||
},
|
|
||||||
sender: {
|
|
||||||
let mut sender = self.sender.clone();
|
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
if let SenderConfig::Smtp(config) = &mut sender {
|
|
||||||
match &mut config.auth {
|
|
||||||
SmtpAuthConfig::Passwd(secret) => {
|
|
||||||
secret.set_keyring_entry_if_undefined(format!("{name}-smtp-passwd"));
|
|
||||||
}
|
|
||||||
SmtpAuthConfig::OAuth2(config) => {
|
|
||||||
config.client_secret.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-smtp-oauth2-client-secret"
|
|
||||||
));
|
|
||||||
config.access_token.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-smtp-oauth2-access-token"
|
|
||||||
));
|
|
||||||
config.refresh_token.set_keyring_entry_if_undefined(format!(
|
|
||||||
"{name}-smtp-oauth2-refresh-token"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
sender
|
|
||||||
},
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
pgp: self.pgp.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,396 +0,0 @@
|
||||||
//! Account handlers module.
|
|
||||||
//!
|
|
||||||
//! This module gathers all account actions triggered by the CLI.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
use email::backend::ImapAuthConfig;
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
use email::sender::SmtpAuthConfig;
|
|
||||||
use email::{
|
|
||||||
account::{
|
|
||||||
sync::{AccountSyncBuilder, AccountSyncProgressEvent},
|
|
||||||
AccountConfig,
|
|
||||||
},
|
|
||||||
backend::BackendConfig,
|
|
||||||
sender::SenderConfig,
|
|
||||||
};
|
|
||||||
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
|
|
||||||
use log::{info, trace, warn};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use std::{collections::HashMap, sync::Mutex};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::{
|
|
||||||
wizard::{prompt_passwd, prompt_secret},
|
|
||||||
DeserializedConfig,
|
|
||||||
},
|
|
||||||
printer::{PrintTableOpts, Printer},
|
|
||||||
Accounts,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
|
||||||
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
|
||||||
ProgressStyle::with_template(
|
|
||||||
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
|
|
||||||
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Configure the current selected account
|
|
||||||
pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> {
|
|
||||||
info!("entering the configure account handler");
|
|
||||||
|
|
||||||
if reset {
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
if let BackendConfig::Imap(imap_config) = &config.backend {
|
|
||||||
let reset = match &imap_config.auth {
|
|
||||||
ImapAuthConfig::Passwd(passwd) => passwd.reset(),
|
|
||||||
ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(),
|
|
||||||
};
|
|
||||||
if let Err(err) = reset {
|
|
||||||
warn!("error while resetting imap secrets, skipping it");
|
|
||||||
warn!("{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
if let SenderConfig::Smtp(smtp_config) = &config.sender {
|
|
||||||
let reset = match &smtp_config.auth {
|
|
||||||
SmtpAuthConfig::Passwd(passwd) => passwd.reset(),
|
|
||||||
SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(),
|
|
||||||
};
|
|
||||||
if let Err(err) = reset {
|
|
||||||
warn!("error while resetting smtp secrets, skipping it");
|
|
||||||
warn!("{err}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
config.pgp.reset().await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
if let BackendConfig::Imap(imap_config) = &config.backend {
|
|
||||||
match &imap_config.auth {
|
|
||||||
ImapAuthConfig::Passwd(passwd) => {
|
|
||||||
passwd.configure(|| prompt_passwd("IMAP password")).await
|
|
||||||
}
|
|
||||||
ImapAuthConfig::OAuth2(oauth2) => {
|
|
||||||
oauth2
|
|
||||||
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
if let SenderConfig::Smtp(smtp_config) = &config.sender {
|
|
||||||
match &smtp_config.auth {
|
|
||||||
SmtpAuthConfig::Passwd(passwd) => {
|
|
||||||
passwd.configure(|| prompt_passwd("SMTP password")).await
|
|
||||||
}
|
|
||||||
SmtpAuthConfig::OAuth2(oauth2) => {
|
|
||||||
oauth2
|
|
||||||
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
config
|
|
||||||
.pgp
|
|
||||||
.configure(&config.email, || prompt_passwd("PGP secret key password"))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Account successfully {}configured!",
|
|
||||||
if reset { "re" } else { "" }
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lists all accounts.
|
|
||||||
pub fn list<'a, P: Printer>(
|
|
||||||
max_width: Option<usize>,
|
|
||||||
config: &AccountConfig,
|
|
||||||
deserialized_config: &DeserializedConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("entering the list accounts handler");
|
|
||||||
|
|
||||||
let accounts: Accounts = deserialized_config.accounts.iter().into();
|
|
||||||
trace!("accounts: {:?}", accounts);
|
|
||||||
|
|
||||||
printer.print_table(
|
|
||||||
Box::new(accounts),
|
|
||||||
PrintTableOpts {
|
|
||||||
format: &config.email_reading_format,
|
|
||||||
max_width,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
info!("<< account list handler");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Synchronizes the account defined using argument `-a|--account`. If
|
|
||||||
/// no account given, synchronizes the default one.
|
|
||||||
pub async fn sync<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
sync_builder: AccountSyncBuilder,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
info!("entering the sync accounts handler");
|
|
||||||
trace!("dry run: {dry_run}");
|
|
||||||
|
|
||||||
if dry_run {
|
|
||||||
let report = sync_builder.sync().await?;
|
|
||||||
let mut hunks_count = report.folders_patch.len();
|
|
||||||
|
|
||||||
if !report.folders_patch.is_empty() {
|
|
||||||
printer.print_log("Folders patch:")?;
|
|
||||||
for (hunk, _) in report.folders_patch {
|
|
||||||
printer.print_log(format!(" - {hunk}"))?;
|
|
||||||
}
|
|
||||||
printer.print_log("")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !report.emails_patch.is_empty() {
|
|
||||||
printer.print_log("Envelopes patch:")?;
|
|
||||||
for (hunk, _) in report.emails_patch {
|
|
||||||
hunks_count += 1;
|
|
||||||
printer.print_log(format!(" - {hunk}"))?;
|
|
||||||
}
|
|
||||||
printer.print_log("")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print(format!(
|
|
||||||
"Estimated patch length for account to be synchronized: {hunks_count}",
|
|
||||||
))?;
|
|
||||||
} else if printer.is_json() {
|
|
||||||
sync_builder.sync().await?;
|
|
||||||
printer.print("Account successfully synchronized!")?;
|
|
||||||
} else {
|
|
||||||
let multi = MultiProgress::new();
|
|
||||||
let sub_progresses = Mutex::new(HashMap::new());
|
|
||||||
let main_progress = multi.add(
|
|
||||||
ProgressBar::new(100)
|
|
||||||
.with_style(MAIN_PROGRESS_STYLE.clone())
|
|
||||||
.with_message("Synchronizing folders…"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Force the progress bar to show
|
|
||||||
main_progress.set_position(0);
|
|
||||||
|
|
||||||
let report = sync_builder
|
|
||||||
.with_on_progress(move |evt| {
|
|
||||||
use AccountSyncProgressEvent::*;
|
|
||||||
Ok(match evt {
|
|
||||||
ApplyFolderPatches(..) => {
|
|
||||||
main_progress.inc(3);
|
|
||||||
}
|
|
||||||
ApplyEnvelopePatches(patches) => {
|
|
||||||
let mut envelopes_progresses = sub_progresses.lock().unwrap();
|
|
||||||
let patches_len = patches.values().fold(0, |sum, patch| sum + patch.len());
|
|
||||||
main_progress.set_length((110 * patches_len / 100) as u64);
|
|
||||||
main_progress.set_position((5 * patches_len / 100) as u64);
|
|
||||||
main_progress.set_message("Synchronizing envelopes…");
|
|
||||||
|
|
||||||
for (folder, patch) in patches {
|
|
||||||
let progress = ProgressBar::new(patch.len() as u64)
|
|
||||||
.with_style(SUB_PROGRESS_STYLE.clone())
|
|
||||||
.with_prefix(folder.clone())
|
|
||||||
.with_finish(ProgressFinish::AndClear);
|
|
||||||
let progress = multi.add(progress);
|
|
||||||
envelopes_progresses.insert(folder, progress.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApplyEnvelopeHunk(hunk) => {
|
|
||||||
main_progress.inc(1);
|
|
||||||
let mut progresses = sub_progresses.lock().unwrap();
|
|
||||||
if let Some(progress) = progresses.get_mut(hunk.folder()) {
|
|
||||||
progress.inc(1);
|
|
||||||
if progress.position() == (progress.length().unwrap() - 1) {
|
|
||||||
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
|
|
||||||
} else {
|
|
||||||
progress.set_message(format!("{hunk}…"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ApplyEnvelopeCachePatch(_patch) => {
|
|
||||||
main_progress.set_length(100);
|
|
||||||
main_progress.set_position(95);
|
|
||||||
main_progress.set_message("Saving cache database…");
|
|
||||||
}
|
|
||||||
ExpungeFolders(folders) => {
|
|
||||||
let mut progresses = sub_progresses.lock().unwrap();
|
|
||||||
for progress in progresses.values() {
|
|
||||||
progress.finish_and_clear()
|
|
||||||
}
|
|
||||||
progresses.clear();
|
|
||||||
|
|
||||||
main_progress.set_position(100);
|
|
||||||
main_progress.set_message(format!("Expunging {} folders…", folders.len()));
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.sync()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let folders_patch_err = report
|
|
||||||
.folders_patch
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if !folders_patch_err.is_empty() {
|
|
||||||
printer.print_log("")?;
|
|
||||||
printer.print_log("Errors occurred while applying the folders patch:")?;
|
|
||||||
folders_patch_err
|
|
||||||
.iter()
|
|
||||||
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(err) = report.folders_cache_patch.1 {
|
|
||||||
printer.print_log("")?;
|
|
||||||
printer.print_log(format!(
|
|
||||||
"Error occurred while applying the folder cache patch: {err}"
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let envelopes_patch_err = report
|
|
||||||
.emails_patch
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if !envelopes_patch_err.is_empty() {
|
|
||||||
printer.print_log("")?;
|
|
||||||
printer.print_log("Errors occurred while applying the envelopes patch:")?;
|
|
||||||
for (hunk, err) in folders_patch_err {
|
|
||||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(err) = report.emails_cache_patch.1 {
|
|
||||||
printer.print_log("")?;
|
|
||||||
printer.print_log(format!(
|
|
||||||
"Error occurred while applying the envelopes cache patch: {err}"
|
|
||||||
))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print("Account successfully synchronized!")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use email::{account::AccountConfig, backend::ImapConfig};
|
|
||||||
use std::{collections::HashMap, fmt::Debug, io};
|
|
||||||
use termcolor::ColorSpec;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
account::DeserializedAccountConfig,
|
|
||||||
printer::{Print, PrintTable, WriteColor},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_should_match_cmds_accounts() {
|
|
||||||
#[derive(Debug, Default, Clone)]
|
|
||||||
struct StringWriter {
|
|
||||||
content: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl io::Write for StringWriter {
|
|
||||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
|
||||||
self.content
|
|
||||||
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
|
|
||||||
Ok(buf.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
self.content = String::default();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl termcolor::WriteColor for StringWriter {
|
|
||||||
fn supports_color(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
|
|
||||||
io::Result::Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self) -> io::Result<()> {
|
|
||||||
io::Result::Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteColor for StringWriter {}
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct PrinterServiceTest {
|
|
||||||
pub writer: StringWriter,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Printer for PrinterServiceTest {
|
|
||||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
|
||||||
&mut self,
|
|
||||||
data: Box<T>,
|
|
||||||
opts: PrintTableOpts,
|
|
||||||
) -> Result<()> {
|
|
||||||
data.print_table(&mut self.writer, opts)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
fn is_json(&self) -> bool {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut printer = PrinterServiceTest::default();
|
|
||||||
let config = AccountConfig::default();
|
|
||||||
let deserialized_config = DeserializedConfig {
|
|
||||||
accounts: HashMap::from_iter([(
|
|
||||||
"account-1".into(),
|
|
||||||
DeserializedAccountConfig {
|
|
||||||
default: Some(true),
|
|
||||||
backend: BackendConfig::Imap(ImapConfig::default()),
|
|
||||||
..DeserializedAccountConfig::default()
|
|
||||||
},
|
|
||||||
)]),
|
|
||||||
..DeserializedConfig::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
concat![
|
|
||||||
"\n",
|
|
||||||
"NAME │BACKEND │DEFAULT \n",
|
|
||||||
"account-1 │imap │yes \n",
|
|
||||||
"\n"
|
|
||||||
],
|
|
||||||
printer.writer.content
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
pub mod account;
|
|
||||||
pub mod accounts;
|
|
||||||
pub mod args;
|
|
||||||
pub mod config;
|
|
||||||
pub mod handlers;
|
|
||||||
pub(crate) mod wizard;
|
|
||||||
|
|
||||||
pub use account::*;
|
|
||||||
pub use accounts::*;
|
|
||||||
pub use config::*;
|
|
|
@ -1,39 +0,0 @@
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use dialoguer::Input;
|
|
||||||
use email_address::EmailAddress;
|
|
||||||
|
|
||||||
use crate::{backend, config::wizard::THEME, sender};
|
|
||||||
|
|
||||||
use super::DeserializedAccountConfig;
|
|
||||||
|
|
||||||
pub(crate) async fn configure() -> Result<Option<(String, DeserializedAccountConfig)>> {
|
|
||||||
let mut config = DeserializedAccountConfig::default();
|
|
||||||
|
|
||||||
let account_name = Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Account name")
|
|
||||||
.default(String::from("Personal"))
|
|
||||||
.interact()?;
|
|
||||||
|
|
||||||
config.email = Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Email address")
|
|
||||||
.validate_with(|email: &String| {
|
|
||||||
if EmailAddress::is_valid(email) {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(anyhow!("Invalid email address: {email}"))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.interact()?;
|
|
||||||
|
|
||||||
config.display_name = Some(
|
|
||||||
Input::with_theme(&*THEME)
|
|
||||||
.with_prompt("Full display name")
|
|
||||||
.interact()?,
|
|
||||||
);
|
|
||||||
|
|
||||||
config.backend = backend::wizard::configure(&account_name, &config.email).await?;
|
|
||||||
|
|
||||||
config.sender = sender::wizard::configure(&account_name, &config.email).await?;
|
|
||||||
|
|
||||||
Ok(Some((account_name, config)))
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
||||||
pub(crate) mod wizard;
|
|
|
@ -1,6 +0,0 @@
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
pub mod imap;
|
|
||||||
pub mod maildir;
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
pub mod notmuch;
|
|
||||||
pub(crate) mod wizard;
|
|
|
@ -1,44 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use dialoguer::Select;
|
|
||||||
use email::backend::BackendConfig;
|
|
||||||
|
|
||||||
use crate::config::wizard::THEME;
|
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
use super::imap;
|
|
||||||
use super::maildir;
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
use super::notmuch;
|
|
||||||
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
const IMAP: &str = "IMAP";
|
|
||||||
const MAILDIR: &str = "Maildir";
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
const NOTMUCH: &str = "Notmuch";
|
|
||||||
const NONE: &str = "None";
|
|
||||||
|
|
||||||
const BACKENDS: &[&str] = &[
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
IMAP,
|
|
||||||
MAILDIR,
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
NOTMUCH,
|
|
||||||
NONE,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
|
|
||||||
let backend = Select::with_theme(&*THEME)
|
|
||||||
.with_prompt("Email backend")
|
|
||||||
.items(BACKENDS)
|
|
||||||
.default(0)
|
|
||||||
.interact_opt()?;
|
|
||||||
|
|
||||||
match backend {
|
|
||||||
#[cfg(feature = "imap-backend")]
|
|
||||||
Some(idx) if BACKENDS[idx] == IMAP => imap::wizard::configure(account_name, email).await,
|
|
||||||
Some(idx) if BACKENDS[idx] == MAILDIR => maildir::wizard::configure(),
|
|
||||||
#[cfg(feature = "notmuch-backend")]
|
|
||||||
Some(idx) if BACKENDS[idx] == NOTMUCH => notmuch::wizard::configure(),
|
|
||||||
_ => Ok(BackendConfig::None),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,438 +0,0 @@
|
||||||
//! Email CLI module.
|
|
||||||
//!
|
|
||||||
//! This module contains the command matcher, the subcommands and the
|
|
||||||
//! arguments related to the email domain.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
||||||
|
|
||||||
use crate::{flag, folder, tpl, ui::table};
|
|
||||||
|
|
||||||
const ARG_CRITERIA: &str = "criterion";
|
|
||||||
const ARG_HEADERS: &str = "headers";
|
|
||||||
const ARG_ID: &str = "id";
|
|
||||||
const ARG_IDS: &str = "ids";
|
|
||||||
const ARG_MIME_TYPE: &str = "mime-type";
|
|
||||||
const ARG_PAGE: &str = "page";
|
|
||||||
const ARG_PAGE_SIZE: &str = "page-size";
|
|
||||||
const ARG_QUERY: &str = "query";
|
|
||||||
const ARG_RAW: &str = "raw";
|
|
||||||
const ARG_REPLY_ALL: &str = "reply-all";
|
|
||||||
const CMD_ATTACHMENTS: &str = "attachments";
|
|
||||||
const CMD_COPY: &str = "copy";
|
|
||||||
const CMD_DELETE: &str = "delete";
|
|
||||||
const CMD_FORWARD: &str = "forward";
|
|
||||||
const CMD_LIST: &str = "list";
|
|
||||||
const CMD_MOVE: &str = "move";
|
|
||||||
const CMD_READ: &str = "read";
|
|
||||||
const CMD_REPLY: &str = "reply";
|
|
||||||
const CMD_SAVE: &str = "save";
|
|
||||||
const CMD_SEARCH: &str = "search";
|
|
||||||
const CMD_SEND: &str = "send";
|
|
||||||
const CMD_SORT: &str = "sort";
|
|
||||||
const CMD_WRITE: &str = "write";
|
|
||||||
|
|
||||||
pub type All = bool;
|
|
||||||
pub type Criteria = String;
|
|
||||||
pub type Folder<'a> = &'a str;
|
|
||||||
pub type Headers<'a> = Vec<&'a str>;
|
|
||||||
pub type Id<'a> = &'a str;
|
|
||||||
pub type Ids<'a> = Vec<&'a str>;
|
|
||||||
pub type Page = usize;
|
|
||||||
pub type PageSize = usize;
|
|
||||||
pub type Query = String;
|
|
||||||
pub type Raw = bool;
|
|
||||||
pub type RawEmail = String;
|
|
||||||
pub type TextMime<'a> = &'a str;
|
|
||||||
|
|
||||||
/// Represents the email commands.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Cmd<'a> {
|
|
||||||
Attachments(Ids<'a>),
|
|
||||||
Copy(Ids<'a>, Folder<'a>),
|
|
||||||
Delete(Ids<'a>),
|
|
||||||
Flag(Option<flag::args::Cmd<'a>>),
|
|
||||||
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
|
||||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
|
||||||
Move(Ids<'a>, Folder<'a>),
|
|
||||||
Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>),
|
|
||||||
Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
|
||||||
Save(RawEmail),
|
|
||||||
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
|
|
||||||
Send(RawEmail),
|
|
||||||
Sort(
|
|
||||||
Criteria,
|
|
||||||
Query,
|
|
||||||
table::args::MaxTableWidth,
|
|
||||||
Option<PageSize>,
|
|
||||||
Page,
|
|
||||||
),
|
|
||||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
|
||||||
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Email command matcher.
|
|
||||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|
||||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
|
||||||
let ids = parse_ids_arg(m);
|
|
||||||
Cmd::Attachments(ids)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
|
||||||
let ids = parse_ids_arg(m);
|
|
||||||
let folder = folder::args::parse_target_arg(m);
|
|
||||||
Cmd::Copy(ids, folder)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
|
|
||||||
let ids = parse_ids_arg(m);
|
|
||||||
Cmd::Delete(ids)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
|
|
||||||
Cmd::Flag(flag::args::matches(m)?)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
|
||||||
let id = parse_id_arg(m);
|
|
||||||
let headers = tpl::args::parse_headers_arg(m);
|
|
||||||
let body = tpl::args::parse_body_arg(m);
|
|
||||||
Cmd::Forward(id, headers, body)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
|
||||||
let max_table_width = table::args::parse_max_width(m);
|
|
||||||
let page_size = parse_page_size_arg(m);
|
|
||||||
let page = parse_page_arg(m);
|
|
||||||
Cmd::List(max_table_width, page_size, page)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
|
|
||||||
let ids = parse_ids_arg(m);
|
|
||||||
let folder = folder::args::parse_target_arg(m);
|
|
||||||
Cmd::Move(ids, folder)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
|
||||||
let ids = parse_ids_arg(m);
|
|
||||||
let mime = parse_mime_type_arg(m);
|
|
||||||
let raw = parse_raw_flag(m);
|
|
||||||
let headers = parse_headers_arg(m);
|
|
||||||
Cmd::Read(ids, mime, raw, headers)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
|
||||||
let id = parse_id_arg(m);
|
|
||||||
let all = parse_reply_all_flag(m);
|
|
||||||
let headers = tpl::args::parse_headers_arg(m);
|
|
||||||
let body = tpl::args::parse_body_arg(m);
|
|
||||||
Cmd::Reply(id, all, headers, body)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
|
||||||
let email = parse_raw_arg(m);
|
|
||||||
Cmd::Save(email)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
|
|
||||||
let max_table_width = table::args::parse_max_width(m);
|
|
||||||
let page_size = parse_page_size_arg(m);
|
|
||||||
let page = parse_page_arg(m);
|
|
||||||
let query = parse_query_arg(m);
|
|
||||||
Cmd::Search(query, max_table_width, page_size, page)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
|
|
||||||
let max_table_width = table::args::parse_max_width(m);
|
|
||||||
let page_size = parse_page_size_arg(m);
|
|
||||||
let page = parse_page_arg(m);
|
|
||||||
let criteria = parse_criteria_arg(m);
|
|
||||||
let query = parse_query_arg(m);
|
|
||||||
Cmd::Sort(criteria, query, max_table_width, page_size, page)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
|
||||||
let email = parse_raw_arg(m);
|
|
||||||
Cmd::Send(email)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
|
|
||||||
Cmd::Tpl(tpl::args::matches(m)?)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
|
||||||
let headers = tpl::args::parse_headers_arg(m);
|
|
||||||
let body = tpl::args::parse_body_arg(m);
|
|
||||||
Cmd::Write(headers, body)
|
|
||||||
} else {
|
|
||||||
Cmd::List(None, None, 0)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(cmd))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email subcommands.
|
|
||||||
pub fn subcmds() -> Vec<Command> {
|
|
||||||
vec![
|
|
||||||
flag::args::subcmds(),
|
|
||||||
tpl::args::subcmds(),
|
|
||||||
vec![
|
|
||||||
Command::new(CMD_ATTACHMENTS)
|
|
||||||
.about("Downloads all emails attachments")
|
|
||||||
.arg(ids_arg()),
|
|
||||||
Command::new(CMD_LIST)
|
|
||||||
.alias("lst")
|
|
||||||
.about("List envelopes")
|
|
||||||
.arg(page_size_arg())
|
|
||||||
.arg(page_arg())
|
|
||||||
.arg(table::args::max_width()),
|
|
||||||
Command::new(CMD_SEARCH)
|
|
||||||
.aliases(["query", "q"])
|
|
||||||
.about("Filter envelopes matching the given query")
|
|
||||||
.arg(page_size_arg())
|
|
||||||
.arg(page_arg())
|
|
||||||
.arg(table::args::max_width())
|
|
||||||
.arg(query_arg()),
|
|
||||||
Command::new(CMD_SORT)
|
|
||||||
.about("Sort envelopes by the given criteria and matching the given query")
|
|
||||||
.arg(page_size_arg())
|
|
||||||
.arg(page_arg())
|
|
||||||
.arg(table::args::max_width())
|
|
||||||
.arg(criteria_arg())
|
|
||||||
.arg(query_arg()),
|
|
||||||
Command::new(CMD_WRITE)
|
|
||||||
.about("Write a new email")
|
|
||||||
.aliases(["new", "n"])
|
|
||||||
.args(tpl::args::args()),
|
|
||||||
Command::new(CMD_SEND)
|
|
||||||
.about("Send a raw email")
|
|
||||||
.arg(raw_arg()),
|
|
||||||
Command::new(CMD_SAVE)
|
|
||||||
.about("Save a raw email")
|
|
||||||
.arg(raw_arg()),
|
|
||||||
Command::new(CMD_READ)
|
|
||||||
.about("Read text bodies of emails")
|
|
||||||
.arg(mime_type_arg())
|
|
||||||
.arg(raw_flag())
|
|
||||||
.arg(headers_arg())
|
|
||||||
.arg(ids_arg()),
|
|
||||||
Command::new(CMD_REPLY)
|
|
||||||
.about("Answer to an email")
|
|
||||||
.arg(reply_all_flag())
|
|
||||||
.args(tpl::args::args())
|
|
||||||
.arg(id_arg()),
|
|
||||||
Command::new(CMD_FORWARD)
|
|
||||||
.aliases(["fwd", "f"])
|
|
||||||
.about("Forward an email")
|
|
||||||
.args(tpl::args::args())
|
|
||||||
.arg(id_arg()),
|
|
||||||
Command::new(CMD_COPY)
|
|
||||||
.alias("cp")
|
|
||||||
.about("Copy emails to the given folder")
|
|
||||||
.arg(folder::args::target_arg())
|
|
||||||
.arg(ids_arg()),
|
|
||||||
Command::new(CMD_MOVE)
|
|
||||||
.alias("mv")
|
|
||||||
.about("Move emails to the given folder")
|
|
||||||
.arg(folder::args::target_arg())
|
|
||||||
.arg(ids_arg()),
|
|
||||||
Command::new(CMD_DELETE)
|
|
||||||
.aliases(["remove", "rm"])
|
|
||||||
.about("Delete emails")
|
|
||||||
.arg(ids_arg()),
|
|
||||||
],
|
|
||||||
]
|
|
||||||
.concat()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email id argument.
|
|
||||||
pub fn id_arg() -> Arg {
|
|
||||||
Arg::new(ARG_ID)
|
|
||||||
.help("Specifies the target email")
|
|
||||||
.value_name("ID")
|
|
||||||
.required(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email id argument parser.
|
|
||||||
pub fn parse_id_arg(matches: &ArgMatches) -> &str {
|
|
||||||
matches.get_one::<String>(ARG_ID).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email ids argument.
|
|
||||||
pub fn ids_arg() -> Arg {
|
|
||||||
Arg::new(ARG_IDS)
|
|
||||||
.help("Email ids")
|
|
||||||
.value_name("IDS")
|
|
||||||
.num_args(1..)
|
|
||||||
.required(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email ids argument parser.
|
|
||||||
pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> {
|
|
||||||
matches
|
|
||||||
.get_many::<String>(ARG_IDS)
|
|
||||||
.unwrap()
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email sort criteria argument.
|
|
||||||
pub fn criteria_arg<'a>() -> Arg {
|
|
||||||
Arg::new(ARG_CRITERIA)
|
|
||||||
.help("Email sorting preferences")
|
|
||||||
.long("criterion")
|
|
||||||
.short('c')
|
|
||||||
.value_name("CRITERION:ORDER")
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
.value_parser([
|
|
||||||
"arrival",
|
|
||||||
"arrival:asc",
|
|
||||||
"arrival:desc",
|
|
||||||
"cc",
|
|
||||||
"cc:asc",
|
|
||||||
"cc:desc",
|
|
||||||
"date",
|
|
||||||
"date:asc",
|
|
||||||
"date:desc",
|
|
||||||
"from",
|
|
||||||
"from:asc",
|
|
||||||
"from:desc",
|
|
||||||
"size",
|
|
||||||
"size:asc",
|
|
||||||
"size:desc",
|
|
||||||
"subject",
|
|
||||||
"subject:asc",
|
|
||||||
"subject:desc",
|
|
||||||
"to",
|
|
||||||
"to:asc",
|
|
||||||
"to:desc",
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email sort criteria argument parser.
|
|
||||||
pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
|
|
||||||
matches
|
|
||||||
.get_many::<String>(ARG_CRITERIA)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email reply all argument.
|
|
||||||
pub fn reply_all_flag() -> Arg {
|
|
||||||
Arg::new(ARG_REPLY_ALL)
|
|
||||||
.help("Include all recipients")
|
|
||||||
.long("all")
|
|
||||||
.short('A')
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email reply all argument parser.
|
|
||||||
pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
|
|
||||||
matches.get_flag(ARG_REPLY_ALL)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the page size argument.
|
|
||||||
fn page_size_arg() -> Arg {
|
|
||||||
Arg::new(ARG_PAGE_SIZE)
|
|
||||||
.help("Page size")
|
|
||||||
.long("page-size")
|
|
||||||
.short('s')
|
|
||||||
.value_name("INT")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the page size argument parser.
|
|
||||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
|
||||||
matches
|
|
||||||
.get_one::<String>(ARG_PAGE_SIZE)
|
|
||||||
.and_then(|s| s.parse().ok())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the page argument.
|
|
||||||
fn page_arg() -> Arg {
|
|
||||||
Arg::new(ARG_PAGE)
|
|
||||||
.help("Page number")
|
|
||||||
.short('p')
|
|
||||||
.long("page")
|
|
||||||
.value_name("INT")
|
|
||||||
.default_value("1")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the page argument parser.
|
|
||||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
|
||||||
matches
|
|
||||||
.get_one::<String>(ARG_PAGE)
|
|
||||||
.unwrap()
|
|
||||||
.parse()
|
|
||||||
.ok()
|
|
||||||
.map(|page| 1.max(page) - 1)
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email headers argument.
|
|
||||||
pub fn headers_arg() -> Arg {
|
|
||||||
Arg::new(ARG_HEADERS)
|
|
||||||
.help("Shows additional headers with the email")
|
|
||||||
.long("header")
|
|
||||||
.short('H')
|
|
||||||
.value_name("STRING")
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email headers argument parser.
|
|
||||||
pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
|
|
||||||
m.get_many::<String>(ARG_HEADERS)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the raw flag.
|
|
||||||
pub fn raw_flag() -> Arg {
|
|
||||||
Arg::new(ARG_RAW)
|
|
||||||
.help("Returns raw version of email")
|
|
||||||
.long("raw")
|
|
||||||
.short('r')
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the raw flag parser.
|
|
||||||
pub fn parse_raw_flag(m: &ArgMatches) -> bool {
|
|
||||||
m.get_flag(ARG_RAW)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email raw argument.
|
|
||||||
pub fn raw_arg() -> Arg {
|
|
||||||
Arg::new(ARG_RAW).raw(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email raw argument parser.
|
|
||||||
pub fn parse_raw_arg(m: &ArgMatches) -> String {
|
|
||||||
m.get_one::<String>(ARG_RAW).cloned().unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email MIME type argument.
|
|
||||||
pub fn mime_type_arg() -> Arg {
|
|
||||||
Arg::new(ARG_MIME_TYPE)
|
|
||||||
.help("MIME type to use")
|
|
||||||
.short('t')
|
|
||||||
.long("mime-type")
|
|
||||||
.value_name("MIME")
|
|
||||||
.value_parser(["plain", "html"])
|
|
||||||
.default_value("plain")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email MIME type argument parser.
|
|
||||||
pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
|
|
||||||
matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email query argument.
|
|
||||||
pub fn query_arg() -> Arg {
|
|
||||||
Arg::new(ARG_QUERY)
|
|
||||||
.long_help("The query system depends on the backend, see the wiki for more details")
|
|
||||||
.value_name("QUERY")
|
|
||||||
.num_args(1..)
|
|
||||||
.required(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the email query argument parser.
|
|
||||||
pub fn parse_query_arg(matches: &ArgMatches) -> String {
|
|
||||||
matches
|
|
||||||
.get_many::<String>(ARG_QUERY)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
|
||||||
match (cmd.as_str(), escape) {
|
|
||||||
// Next command is an arg and needs to be escaped
|
|
||||||
("subject", _) | ("body", _) | ("text", _) => {
|
|
||||||
cmds.push(cmd.to_string());
|
|
||||||
(true, cmds)
|
|
||||||
}
|
|
||||||
// Escaped arg commands
|
|
||||||
(_, true) => {
|
|
||||||
cmds.push(format!("\"{}\"", cmd));
|
|
||||||
(false, cmds)
|
|
||||||
}
|
|
||||||
// Regular commands
|
|
||||||
(_, false) => {
|
|
||||||
cmds.push(cmd.to_string());
|
|
||||||
(false, cmds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.1
|
|
||||||
.join(" ")
|
|
||||||
}
|
|
|
@ -1,421 +0,0 @@
|
||||||
use anyhow::{anyhow, Context, Result};
|
|
||||||
use atty::Stream;
|
|
||||||
use email::{
|
|
||||||
account::AccountConfig,
|
|
||||||
backend::Backend,
|
|
||||||
email::{template::FilterParts, Flag, Flags, Message, MessageBuilder},
|
|
||||||
sender::Sender,
|
|
||||||
};
|
|
||||||
use log::{debug, trace};
|
|
||||||
use std::{
|
|
||||||
fs,
|
|
||||||
io::{self, BufRead},
|
|
||||||
};
|
|
||||||
use url::Url;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
printer::{PrintTableOpts, Printer},
|
|
||||||
ui::editor,
|
|
||||||
Envelopes, IdMapper,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub async fn attachments<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
let emails = backend.get_emails(&folder, ids.clone()).await?;
|
|
||||||
let mut index = 0;
|
|
||||||
|
|
||||||
let mut emails_count = 0;
|
|
||||||
let mut attachments_count = 0;
|
|
||||||
|
|
||||||
for email in emails.to_vec() {
|
|
||||||
let id = ids.get(index).unwrap();
|
|
||||||
let attachments = email.attachments()?;
|
|
||||||
|
|
||||||
index = index + 1;
|
|
||||||
|
|
||||||
if attachments.is_empty() {
|
|
||||||
printer.print_log(format!("No attachment found for email #{}", id))?;
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
emails_count = emails_count + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print_log(format!(
|
|
||||||
"{} attachment(s) found for email #{}…",
|
|
||||||
attachments.len(),
|
|
||||||
id
|
|
||||||
))?;
|
|
||||||
|
|
||||||
for attachment in attachments {
|
|
||||||
let filename = attachment
|
|
||||||
.filename
|
|
||||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
|
||||||
let filepath = config.download_fpath(&filename)?;
|
|
||||||
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
|
||||||
fs::write(&filepath, &attachment.body).context("cannot download attachment")?;
|
|
||||||
attachments_count = attachments_count + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match attachments_count {
|
|
||||||
0 => printer.print("No attachment found!"),
|
|
||||||
1 => printer.print("Downloaded 1 attachment!"),
|
|
||||||
n => printer.print(format!(
|
|
||||||
"Downloaded {} attachment(s) from {} email(s)!",
|
|
||||||
n, emails_count,
|
|
||||||
)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn copy<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
from_folder: &str,
|
|
||||||
to_folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.copy_emails(&from_folder, &to_folder, ids).await?;
|
|
||||||
printer.print("Email(s) successfully copied!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.delete_emails(&folder, ids).await?;
|
|
||||||
printer.print("Email(s) successfully deleted!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn forward<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
folder: &str,
|
|
||||||
id: &str,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids([id])?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let tpl = backend
|
|
||||||
.get_emails(&folder, ids)
|
|
||||||
.await?
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
|
||||||
.to_forward_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.build()
|
|
||||||
.await?;
|
|
||||||
trace!("initial template: {tpl}");
|
|
||||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn list<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
max_width: Option<usize>,
|
|
||||||
page_size: Option<usize>,
|
|
||||||
page: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
|
||||||
debug!("page size: {}", page_size);
|
|
||||||
|
|
||||||
let envelopes = Envelopes::from_backend(
|
|
||||||
config,
|
|
||||||
id_mapper,
|
|
||||||
backend.list_envelopes(&folder, page_size, page).await?,
|
|
||||||
)?;
|
|
||||||
trace!("envelopes: {:?}", envelopes);
|
|
||||||
|
|
||||||
printer.print_table(
|
|
||||||
Box::new(envelopes),
|
|
||||||
PrintTableOpts {
|
|
||||||
format: &config.email_reading_format,
|
|
||||||
max_width,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses and edits a message from a [mailto] URL string.
|
|
||||||
///
|
|
||||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
|
||||||
pub async fn mailto<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
printer: &mut P,
|
|
||||||
url: &Url,
|
|
||||||
) -> Result<()> {
|
|
||||||
let mut builder = MessageBuilder::new().to(url.path());
|
|
||||||
|
|
||||||
for (key, val) in url.query_pairs() {
|
|
||||||
match key.to_lowercase().as_bytes() {
|
|
||||||
b"cc" => builder = builder.cc(val.to_string()),
|
|
||||||
b"bcc" => builder = builder.bcc(val.to_string()),
|
|
||||||
b"subject" => builder = builder.subject(val),
|
|
||||||
b"body" => builder = builder.text_body(val),
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tpl = config
|
|
||||||
.generate_tpl_interpreter()
|
|
||||||
.with_show_only_headers(config.email_writing_headers())
|
|
||||||
.build()
|
|
||||||
.from_msg_builder(builder)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn move_<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
from_folder: &str,
|
|
||||||
to_folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.move_emails(&from_folder, &to_folder, ids).await?;
|
|
||||||
printer.print("Email(s) successfully moved!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn read<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
text_mime: &str,
|
|
||||||
raw: bool,
|
|
||||||
headers: Vec<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
let emails = backend.get_emails(&folder, ids).await?;
|
|
||||||
|
|
||||||
let mut glue = "";
|
|
||||||
let mut bodies = String::default();
|
|
||||||
|
|
||||||
for email in emails.to_vec() {
|
|
||||||
bodies.push_str(glue);
|
|
||||||
|
|
||||||
if raw {
|
|
||||||
// emails do not always have valid utf8, uses "lossy" to
|
|
||||||
// display what can be displayed
|
|
||||||
bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned());
|
|
||||||
} else {
|
|
||||||
let tpl: String = email
|
|
||||||
.to_read_tpl(&config, |tpl| match text_mime {
|
|
||||||
"html" => tpl
|
|
||||||
.with_hide_all_headers()
|
|
||||||
.with_filter_parts(FilterParts::Only("text/html".into())),
|
|
||||||
_ => tpl.with_show_additional_headers(&headers),
|
|
||||||
})
|
|
||||||
.await?
|
|
||||||
.into();
|
|
||||||
bodies.push_str(&tpl);
|
|
||||||
}
|
|
||||||
|
|
||||||
glue = "\n\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print(bodies)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reply<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
folder: &str,
|
|
||||||
id: &str,
|
|
||||||
all: bool,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids([id])?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let tpl = backend
|
|
||||||
.get_emails(&folder, ids)
|
|
||||||
.await?
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
|
||||||
.to_reply_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.with_reply_all(all)
|
|
||||||
.build()
|
|
||||||
.await?;
|
|
||||||
trace!("initial template: {tpl}");
|
|
||||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
|
||||||
backend
|
|
||||||
.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
raw_email: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let is_tty = atty::is(Stream::Stdin);
|
|
||||||
let is_json = printer.is_json();
|
|
||||||
let raw_email = if is_tty || is_json {
|
|
||||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
|
||||||
} else {
|
|
||||||
io::stdin()
|
|
||||||
.lock()
|
|
||||||
.lines()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\r\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = backend
|
|
||||||
.add_email(&folder, raw_email.as_bytes(), &Flags::default())
|
|
||||||
.await?;
|
|
||||||
id_mapper.create_alias(id)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn search<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
query: String,
|
|
||||||
max_width: Option<usize>,
|
|
||||||
page_size: Option<usize>,
|
|
||||||
page: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
|
||||||
let envelopes = Envelopes::from_backend(
|
|
||||||
config,
|
|
||||||
id_mapper,
|
|
||||||
backend
|
|
||||||
.search_envelopes(&folder, &query, "", page_size, page)
|
|
||||||
.await?,
|
|
||||||
)?;
|
|
||||||
let opts = PrintTableOpts {
|
|
||||||
format: &config.email_reading_format,
|
|
||||||
max_width,
|
|
||||||
};
|
|
||||||
|
|
||||||
printer.print_table(Box::new(envelopes), opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn sort<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
sort: String,
|
|
||||||
query: String,
|
|
||||||
max_width: Option<usize>,
|
|
||||||
page_size: Option<usize>,
|
|
||||||
page: usize,
|
|
||||||
) -> Result<()> {
|
|
||||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
|
||||||
let envelopes = Envelopes::from_backend(
|
|
||||||
config,
|
|
||||||
id_mapper,
|
|
||||||
backend
|
|
||||||
.search_envelopes(&folder, &query, &sort, page_size, page)
|
|
||||||
.await?,
|
|
||||||
)?;
|
|
||||||
let opts = PrintTableOpts {
|
|
||||||
format: &config.email_reading_format,
|
|
||||||
max_width,
|
|
||||||
};
|
|
||||||
|
|
||||||
printer.print_table(Box::new(envelopes), opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
raw_email: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let folder = config.sent_folder_alias()?;
|
|
||||||
let is_tty = atty::is(Stream::Stdin);
|
|
||||||
let is_json = printer.is_json();
|
|
||||||
let raw_email = if is_tty || is_json {
|
|
||||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
|
||||||
} else {
|
|
||||||
io::stdin()
|
|
||||||
.lock()
|
|
||||||
.lines()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\r\n")
|
|
||||||
};
|
|
||||||
trace!("raw email: {:?}", raw_email);
|
|
||||||
sender.send(raw_email.as_bytes()).await?;
|
|
||||||
if config.email_sending_save_copy {
|
|
||||||
backend
|
|
||||||
.add_email(
|
|
||||||
&folder,
|
|
||||||
raw_email.as_bytes(),
|
|
||||||
&Flags::from_iter([Flag::Seen]),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn write<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tpl = Message::new_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.build()
|
|
||||||
.await?;
|
|
||||||
trace!("initial template: {tpl}");
|
|
||||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
|
@ -1,66 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ui::{Cell, Row, Table},
|
|
||||||
Flag, Flags,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
|
||||||
pub struct Mailbox {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub addr: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
|
||||||
pub struct Envelope {
|
|
||||||
pub id: String,
|
|
||||||
pub flags: Flags,
|
|
||||||
pub subject: String,
|
|
||||||
pub from: Mailbox,
|
|
||||||
pub date: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Table for Envelope {
|
|
||||||
fn head() -> Row {
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new("ID").bold().underline().white())
|
|
||||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
|
||||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
|
||||||
.cell(Cell::new("FROM").bold().underline().white())
|
|
||||||
.cell(Cell::new("DATE").bold().underline().white())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row(&self) -> Row {
|
|
||||||
let id = self.id.to_string();
|
|
||||||
let unseen = !self.flags.contains(&Flag::Seen);
|
|
||||||
let flags = {
|
|
||||||
let mut flags = String::new();
|
|
||||||
flags.push_str(if !unseen { " " } else { "✷" });
|
|
||||||
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
|
||||||
"↵"
|
|
||||||
} else {
|
|
||||||
" "
|
|
||||||
});
|
|
||||||
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
|
||||||
"⚑"
|
|
||||||
} else {
|
|
||||||
" "
|
|
||||||
});
|
|
||||||
flags
|
|
||||||
};
|
|
||||||
let subject = &self.subject;
|
|
||||||
let sender = if let Some(name) = &self.from.name {
|
|
||||||
name
|
|
||||||
} else {
|
|
||||||
&self.from.addr
|
|
||||||
};
|
|
||||||
let date = &self.date;
|
|
||||||
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new(id).bold_if(unseen).red())
|
|
||||||
.cell(Cell::new(flags).bold_if(unseen).white())
|
|
||||||
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
|
||||||
.cell(Cell::new(sender).bold_if(unseen).blue())
|
|
||||||
.cell(Cell::new(date).bold_if(unseen).yellow())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
pub mod envelope;
|
|
||||||
pub mod envelopes;
|
|
||||||
|
|
||||||
pub use envelope::*;
|
|
||||||
pub use envelopes::*;
|
|
|
@ -1,105 +0,0 @@
|
||||||
//! Email flag CLI module.
|
|
||||||
//!
|
|
||||||
//! This module contains the command matcher, the subcommands and the
|
|
||||||
//! arguments related to the email flag domain.
|
|
||||||
|
|
||||||
use ::email::email::{Flag, Flags};
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{Arg, ArgMatches, Command};
|
|
||||||
use log::{debug, info};
|
|
||||||
|
|
||||||
use crate::email;
|
|
||||||
|
|
||||||
const ARG_FLAGS: &str = "flag";
|
|
||||||
|
|
||||||
const CMD_ADD: &str = "add";
|
|
||||||
const CMD_REMOVE: &str = "remove";
|
|
||||||
const CMD_SET: &str = "set";
|
|
||||||
|
|
||||||
pub(crate) const CMD_FLAG: &str = "flags";
|
|
||||||
|
|
||||||
/// Represents the flag commands.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Cmd<'a> {
|
|
||||||
Add(email::args::Ids<'a>, Flags),
|
|
||||||
Remove(email::args::Ids<'a>, Flags),
|
|
||||||
Set(email::args::Ids<'a>, Flags),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the flag command matcher.
|
|
||||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|
||||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
|
|
||||||
debug!("add flags command matched");
|
|
||||||
let ids = email::args::parse_ids_arg(m);
|
|
||||||
let flags = parse_flags_arg(m);
|
|
||||||
Some(Cmd::Add(ids, flags))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_REMOVE) {
|
|
||||||
info!("remove flags command matched");
|
|
||||||
let ids = email::args::parse_ids_arg(m);
|
|
||||||
let flags = parse_flags_arg(m);
|
|
||||||
Some(Cmd::Remove(ids, flags))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
|
|
||||||
debug!("set flags command matched");
|
|
||||||
let ids = email::args::parse_ids_arg(m);
|
|
||||||
let flags = parse_flags_arg(m);
|
|
||||||
Some(Cmd::Set(ids, flags))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the flag subcommands.
|
|
||||||
pub fn subcmds<'a>() -> Vec<Command> {
|
|
||||||
vec![Command::new(CMD_FLAG)
|
|
||||||
.about("Handles email flags")
|
|
||||||
.subcommand_required(true)
|
|
||||||
.arg_required_else_help(true)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_ADD)
|
|
||||||
.about("Adds flags to an email")
|
|
||||||
.arg(email::args::ids_arg())
|
|
||||||
.arg(flags_arg()),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_REMOVE)
|
|
||||||
.aliases(["delete", "del", "d"])
|
|
||||||
.about("Removes flags from an email")
|
|
||||||
.arg(email::args::ids_arg())
|
|
||||||
.arg(flags_arg()),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_SET)
|
|
||||||
.aliases(["change", "c"])
|
|
||||||
.about("Sets flags of an email")
|
|
||||||
.arg(email::args::ids_arg())
|
|
||||||
.arg(flags_arg()),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the flags argument.
|
|
||||||
pub fn flags_arg() -> Arg {
|
|
||||||
Arg::new(ARG_FLAGS)
|
|
||||||
.value_name("FLAGS")
|
|
||||||
.help("The flags")
|
|
||||||
.long_help(
|
|
||||||
"The list of flags.
|
|
||||||
It can be one of: seen, answered, flagged, deleted, or draft.
|
|
||||||
Other flags are considered custom.",
|
|
||||||
)
|
|
||||||
.num_args(1..)
|
|
||||||
.required(true)
|
|
||||||
.last(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the flags argument parser.
|
|
||||||
pub fn parse_flags_arg(matches: &ArgMatches) -> Flags {
|
|
||||||
Flags::from_iter(
|
|
||||||
matches
|
|
||||||
.get_many::<String>(ARG_FLAGS)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map(String::as_str)
|
|
||||||
.map(Flag::from),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
/// Represents the flag variants.
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
|
|
||||||
pub enum Flag {
|
|
||||||
Seen,
|
|
||||||
Answered,
|
|
||||||
Flagged,
|
|
||||||
Deleted,
|
|
||||||
Draft,
|
|
||||||
Custom(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&email::email::Flag> for Flag {
|
|
||||||
fn from(flag: &email::email::Flag) -> Self {
|
|
||||||
use email::email::Flag::*;
|
|
||||||
match flag {
|
|
||||||
Seen => Flag::Seen,
|
|
||||||
Answered => Flag::Answered,
|
|
||||||
Flagged => Flag::Flagged,
|
|
||||||
Deleted => Flag::Deleted,
|
|
||||||
Draft => Flag::Draft,
|
|
||||||
Custom(flag) => Flag::Custom(flag.clone()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
use std::{collections::HashSet, ops};
|
|
||||||
|
|
||||||
use crate::Flag;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
|
||||||
pub struct Flags(pub HashSet<Flag>);
|
|
||||||
|
|
||||||
impl ops::Deref for Flags {
|
|
||||||
type Target = HashSet<Flag>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<email::email::Flags> for Flags {
|
|
||||||
fn from(flags: email::email::Flags) -> Self {
|
|
||||||
Flags(flags.iter().map(Flag::from).collect())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,46 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use email::{backend::Backend, email::Flags};
|
|
||||||
|
|
||||||
use crate::{printer::Printer, IdMapper};
|
|
||||||
|
|
||||||
pub async fn add<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
flags: &Flags,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.add_flags(folder, ids, flags).await?;
|
|
||||||
printer.print("Flag(s) successfully added!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
flags: &Flags,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.set_flags(folder, ids, flags).await?;
|
|
||||||
printer.print("Flag(s) successfully set!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn remove<P: Printer>(
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
ids: Vec<&str>,
|
|
||||||
flags: &Flags,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids(ids)?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
backend.remove_flags(folder, ids, flags).await?;
|
|
||||||
printer.print("Flag(s) successfully removed!")
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
||||||
|
|
||||||
pub mod flag;
|
|
||||||
pub use flag::*;
|
|
||||||
|
|
||||||
pub mod flags;
|
|
||||||
pub use flags::*;
|
|
|
@ -1,237 +0,0 @@
|
||||||
//! Folder CLI module.
|
|
||||||
//!
|
|
||||||
//! This module provides subcommands, arguments and a command matcher
|
|
||||||
//! related to the folder domain.
|
|
||||||
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{self, Arg, ArgAction, ArgMatches, Command};
|
|
||||||
use log::{debug, info};
|
|
||||||
|
|
||||||
use crate::ui::table;
|
|
||||||
|
|
||||||
const ARG_ALL: &str = "all";
|
|
||||||
const ARG_EXCLUDE: &str = "exclude";
|
|
||||||
const ARG_INCLUDE: &str = "include";
|
|
||||||
const ARG_SOURCE: &str = "source";
|
|
||||||
const ARG_TARGET: &str = "target";
|
|
||||||
const CMD_CREATE: &str = "create";
|
|
||||||
const CMD_DELETE: &str = "delete";
|
|
||||||
const CMD_EXPUNGE: &str = "expunge";
|
|
||||||
const CMD_FOLDERS: &str = "folders";
|
|
||||||
const CMD_LIST: &str = "list";
|
|
||||||
|
|
||||||
/// Represents the folder commands.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Cmd {
|
|
||||||
Create,
|
|
||||||
List(table::args::MaxTableWidth),
|
|
||||||
Expunge,
|
|
||||||
Delete,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folder command matcher.
|
|
||||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|
||||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
|
|
||||||
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
|
|
||||||
info!("expunge folder subcommand matched");
|
|
||||||
Some(Cmd::Expunge)
|
|
||||||
} else if let Some(_) = m.subcommand_matches(CMD_CREATE) {
|
|
||||||
debug!("create folder command matched");
|
|
||||||
Some(Cmd::Create)
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
|
||||||
debug!("list folders command matched");
|
|
||||||
let max_table_width = table::args::parse_max_width(m);
|
|
||||||
Some(Cmd::List(max_table_width))
|
|
||||||
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
|
|
||||||
debug!("delete folder command matched");
|
|
||||||
Some(Cmd::Delete)
|
|
||||||
} else {
|
|
||||||
info!("no folder subcommand matched, falling back to subcommand list");
|
|
||||||
Some(Cmd::List(None))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folder subcommand.
|
|
||||||
pub fn subcmd() -> Command {
|
|
||||||
Command::new(CMD_FOLDERS)
|
|
||||||
.about("Manage folders")
|
|
||||||
.subcommands([
|
|
||||||
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
|
|
||||||
Command::new(CMD_CREATE)
|
|
||||||
.aliases(["add", "new"])
|
|
||||||
.about("Create a new folder"),
|
|
||||||
Command::new(CMD_LIST)
|
|
||||||
.about("List folders")
|
|
||||||
.arg(table::args::max_width()),
|
|
||||||
Command::new(CMD_DELETE)
|
|
||||||
.aliases(["remove", "rm"])
|
|
||||||
.about("Delete a folder with all its emails"),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the source folder argument.
|
|
||||||
pub fn source_arg() -> Arg {
|
|
||||||
Arg::new(ARG_SOURCE)
|
|
||||||
.help("Set the source folder")
|
|
||||||
.long("folder")
|
|
||||||
.short('f')
|
|
||||||
.global(true)
|
|
||||||
.value_name("SOURCE")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the source folder argument parser.
|
|
||||||
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
|
|
||||||
matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the all folders argument.
|
|
||||||
pub fn all_arg(help: &'static str) -> Arg {
|
|
||||||
Arg::new(ARG_ALL)
|
|
||||||
.help(help)
|
|
||||||
.long("all-folders")
|
|
||||||
.alias("all")
|
|
||||||
.short('A')
|
|
||||||
.action(ArgAction::SetTrue)
|
|
||||||
.conflicts_with(ARG_SOURCE)
|
|
||||||
.conflicts_with(ARG_INCLUDE)
|
|
||||||
.conflicts_with(ARG_EXCLUDE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the all folders argument parser.
|
|
||||||
pub fn parse_all_arg(m: &ArgMatches) -> bool {
|
|
||||||
m.get_flag(ARG_ALL)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folders to include argument.
|
|
||||||
pub fn include_arg(help: &'static str) -> Arg {
|
|
||||||
Arg::new(ARG_INCLUDE)
|
|
||||||
.help(help)
|
|
||||||
.long("include-folder")
|
|
||||||
.alias("only")
|
|
||||||
.short('F')
|
|
||||||
.value_name("FOLDER")
|
|
||||||
.num_args(1..)
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
.conflicts_with(ARG_SOURCE)
|
|
||||||
.conflicts_with(ARG_ALL)
|
|
||||||
.conflicts_with(ARG_EXCLUDE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folders to include argument parser.
|
|
||||||
pub fn parse_include_arg(m: &ArgMatches) -> HashSet<String> {
|
|
||||||
m.get_many::<String>(ARG_INCLUDE)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folders to exclude argument.
|
|
||||||
pub fn exclude_arg(help: &'static str) -> Arg {
|
|
||||||
Arg::new(ARG_EXCLUDE)
|
|
||||||
.help(help)
|
|
||||||
.long("exclude-folder")
|
|
||||||
.alias("except")
|
|
||||||
.short('x')
|
|
||||||
.value_name("FOLDER")
|
|
||||||
.num_args(1..)
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
.conflicts_with(ARG_SOURCE)
|
|
||||||
.conflicts_with(ARG_ALL)
|
|
||||||
.conflicts_with(ARG_INCLUDE)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the folders to exclude argument parser.
|
|
||||||
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
|
|
||||||
m.get_many::<String>(ARG_EXCLUDE)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the target folder argument.
|
|
||||||
pub fn target_arg() -> Arg {
|
|
||||||
Arg::new(ARG_TARGET)
|
|
||||||
.help("Specifies the target folder")
|
|
||||||
.value_name("TARGET")
|
|
||||||
.required(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the target folder argument parser.
|
|
||||||
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
|
|
||||||
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use clap::{error::ErrorKind, Command};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_should_match_cmds() {
|
|
||||||
let arg = Command::new("himalaya")
|
|
||||||
.subcommand(subcmd())
|
|
||||||
.get_matches_from(&["himalaya", "folders"]);
|
|
||||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
|
||||||
|
|
||||||
let arg = Command::new("himalaya")
|
|
||||||
.subcommand(subcmd())
|
|
||||||
.get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]);
|
|
||||||
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_should_match_source_arg() {
|
|
||||||
macro_rules! get_matches_from {
|
|
||||||
($($arg:expr),*) => {
|
|
||||||
Command::new("himalaya")
|
|
||||||
.arg(source_arg())
|
|
||||||
.get_matches_from(&["himalaya", $($arg,)*])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let app = get_matches_from![];
|
|
||||||
assert_eq!(None, app.get_one::<String>(ARG_SOURCE).map(String::as_str));
|
|
||||||
|
|
||||||
let app = get_matches_from!["-f", "SOURCE"];
|
|
||||||
assert_eq!(
|
|
||||||
Some("SOURCE"),
|
|
||||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
|
||||||
);
|
|
||||||
|
|
||||||
let app = get_matches_from!["--folder", "SOURCE"];
|
|
||||||
assert_eq!(
|
|
||||||
Some("SOURCE"),
|
|
||||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_should_match_target_arg() {
|
|
||||||
macro_rules! get_matches_from {
|
|
||||||
($($arg:expr),*) => {
|
|
||||||
Command::new("himalaya")
|
|
||||||
.arg(target_arg())
|
|
||||||
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let app = get_matches_from![];
|
|
||||||
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
|
|
||||||
|
|
||||||
let app = get_matches_from!["TARGET"];
|
|
||||||
assert_eq!(
|
|
||||||
Some("TARGET"),
|
|
||||||
app.unwrap()
|
|
||||||
.get_one::<String>(ARG_TARGET)
|
|
||||||
.map(String::as_str)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::ui::{Cell, Row, Table};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
|
||||||
pub struct Folder {
|
|
||||||
pub name: String,
|
|
||||||
pub desc: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&email::folder::Folder> for Folder {
|
|
||||||
fn from(folder: &email::folder::Folder) -> Self {
|
|
||||||
Folder {
|
|
||||||
name: folder.name.clone(),
|
|
||||||
desc: folder.desc.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Table for Folder {
|
|
||||||
fn head() -> Row {
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new("NAME").bold().underline().white())
|
|
||||||
.cell(Cell::new("DESC").bold().underline().white())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn row(&self) -> Row {
|
|
||||||
Row::new()
|
|
||||||
.cell(Cell::new(&self.name).blue())
|
|
||||||
.cell(Cell::new(&self.desc).green())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::ops;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
|
||||||
ui::Table,
|
|
||||||
Folder,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
|
||||||
pub struct Folders(Vec<Folder>);
|
|
||||||
|
|
||||||
impl ops::Deref for Folders {
|
|
||||||
type Target = Vec<Folder>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<email::folder::Folders> for Folders {
|
|
||||||
fn from(folders: email::folder::Folders) -> Self {
|
|
||||||
Folders(folders.iter().map(Folder::from).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrintTable for Folders {
|
|
||||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
|
||||||
writeln!(writer)?;
|
|
||||||
Table::print(writer, self, opts)?;
|
|
||||||
writeln!(writer)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
pub mod folder;
|
|
||||||
pub use folder::*;
|
|
||||||
|
|
||||||
pub mod folders;
|
|
||||||
pub use folders::*;
|
|
||||||
|
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
|
@ -1,16 +0,0 @@
|
||||||
pub mod account;
|
|
||||||
pub mod backend;
|
|
||||||
pub mod email;
|
|
||||||
pub mod envelope;
|
|
||||||
pub mod flag;
|
|
||||||
pub mod folder;
|
|
||||||
pub mod sender;
|
|
||||||
pub mod tpl;
|
|
||||||
|
|
||||||
pub use self::account::{args, handlers, Account, Accounts};
|
|
||||||
pub use self::backend::*;
|
|
||||||
pub use self::email::*;
|
|
||||||
pub use self::envelope::*;
|
|
||||||
pub use self::flag::*;
|
|
||||||
pub use self::folder::*;
|
|
||||||
pub use self::tpl::*;
|
|
|
@ -1,4 +0,0 @@
|
||||||
pub mod sendmail;
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
pub mod smtp;
|
|
||||||
pub(crate) mod wizard;
|
|
|
@ -1,36 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use dialoguer::Select;
|
|
||||||
use email::sender::SenderConfig;
|
|
||||||
|
|
||||||
use crate::config::wizard::THEME;
|
|
||||||
|
|
||||||
use super::sendmail;
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
use super::smtp;
|
|
||||||
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
const SMTP: &str = "SMTP";
|
|
||||||
const SENDMAIL: &str = "Sendmail";
|
|
||||||
const NONE: &str = "None";
|
|
||||||
|
|
||||||
const SENDERS: &[&str] = &[
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
SMTP,
|
|
||||||
SENDMAIL,
|
|
||||||
NONE,
|
|
||||||
];
|
|
||||||
|
|
||||||
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
|
|
||||||
let sender = Select::with_theme(&*THEME)
|
|
||||||
.with_prompt("Email sender")
|
|
||||||
.items(SENDERS)
|
|
||||||
.default(0)
|
|
||||||
.interact_opt()?;
|
|
||||||
|
|
||||||
match sender {
|
|
||||||
#[cfg(feature = "smtp-sender")]
|
|
||||||
Some(n) if SENDERS[n] == SMTP => smtp::wizard::configure(account_name, email).await,
|
|
||||||
Some(n) if SENDERS[n] == SENDMAIL => sendmail::wizard::configure(),
|
|
||||||
_ => Ok(SenderConfig::None),
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,148 +0,0 @@
|
||||||
//! Module related to email template CLI.
|
|
||||||
//!
|
|
||||||
//! This module provides subcommands, arguments and a command matcher
|
|
||||||
//! related to email templating.
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
|
||||||
use log::warn;
|
|
||||||
|
|
||||||
use crate::email;
|
|
||||||
|
|
||||||
const ARG_BODY: &str = "body";
|
|
||||||
const ARG_HEADERS: &str = "headers";
|
|
||||||
const ARG_TPL: &str = "template";
|
|
||||||
const CMD_FORWARD: &str = "forward";
|
|
||||||
const CMD_REPLY: &str = "reply";
|
|
||||||
const CMD_SAVE: &str = "save";
|
|
||||||
const CMD_SEND: &str = "send";
|
|
||||||
const CMD_WRITE: &str = "write";
|
|
||||||
|
|
||||||
pub const CMD_TPL: &str = "template";
|
|
||||||
|
|
||||||
pub type RawTpl = String;
|
|
||||||
pub type Headers<'a> = Option<Vec<(&'a str, &'a str)>>;
|
|
||||||
pub type Body<'a> = Option<&'a str>;
|
|
||||||
|
|
||||||
/// Represents the template commands.
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub enum Cmd<'a> {
|
|
||||||
Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
|
|
||||||
Write(Headers<'a>, Body<'a>),
|
|
||||||
Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
|
|
||||||
Save(RawTpl),
|
|
||||||
Send(RawTpl),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the template command matcher.
|
|
||||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|
||||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
|
||||||
let id = email::args::parse_id_arg(m);
|
|
||||||
let headers = parse_headers_arg(m);
|
|
||||||
let body = parse_body_arg(m);
|
|
||||||
Some(Cmd::Forward(id, headers, body))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
|
||||||
let id = email::args::parse_id_arg(m);
|
|
||||||
let all = email::args::parse_reply_all_flag(m);
|
|
||||||
let headers = parse_headers_arg(m);
|
|
||||||
let body = parse_body_arg(m);
|
|
||||||
Some(Cmd::Reply(id, all, headers, body))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
|
||||||
let raw_tpl = parse_raw_arg(m);
|
|
||||||
Some(Cmd::Save(raw_tpl))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
|
||||||
let raw_tpl = parse_raw_arg(m);
|
|
||||||
Some(Cmd::Send(raw_tpl))
|
|
||||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
|
||||||
let headers = parse_headers_arg(m);
|
|
||||||
let body = parse_body_arg(m);
|
|
||||||
Some(Cmd::Write(headers, body))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the template subcommands.
|
|
||||||
pub fn subcmds<'a>() -> Vec<Command> {
|
|
||||||
vec![Command::new(CMD_TPL)
|
|
||||||
.alias("tpl")
|
|
||||||
.about("Handles email templates")
|
|
||||||
.subcommand_required(true)
|
|
||||||
.arg_required_else_help(true)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_FORWARD)
|
|
||||||
.alias("fwd")
|
|
||||||
.about("Generates a template for forwarding an email")
|
|
||||||
.arg(email::args::id_arg())
|
|
||||||
.args(&args()),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_REPLY)
|
|
||||||
.about("Generates a template for replying to an email")
|
|
||||||
.arg(email::args::id_arg())
|
|
||||||
.arg(email::args::reply_all_flag())
|
|
||||||
.args(&args()),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_SAVE)
|
|
||||||
.about("Compiles the template into a valid email then saves it")
|
|
||||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_SEND)
|
|
||||||
.about("Compiles the template into a valid email then sends it")
|
|
||||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
|
||||||
)
|
|
||||||
.subcommand(
|
|
||||||
Command::new(CMD_WRITE)
|
|
||||||
.aliases(["new", "n"])
|
|
||||||
.about("Generates a template for writing a new email")
|
|
||||||
.args(&args()),
|
|
||||||
)]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the template arguments.
|
|
||||||
pub fn args() -> Vec<Arg> {
|
|
||||||
vec![
|
|
||||||
Arg::new(ARG_HEADERS)
|
|
||||||
.help("Overrides a specific header")
|
|
||||||
.short('H')
|
|
||||||
.long("header")
|
|
||||||
.value_name("KEY:VAL")
|
|
||||||
.action(ArgAction::Append),
|
|
||||||
Arg::new(ARG_BODY)
|
|
||||||
.help("Overrides the body")
|
|
||||||
.short('B')
|
|
||||||
.long("body")
|
|
||||||
.value_name("STRING"),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the template headers argument parser.
|
|
||||||
pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> {
|
|
||||||
m.get_many::<String>(ARG_HEADERS).map(|h| {
|
|
||||||
h.filter_map(|h| match h.split_once(':') {
|
|
||||||
Some((key, val)) => Some((key, val.trim())),
|
|
||||||
None => {
|
|
||||||
warn!("invalid raw header {h:?}, skipping it");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the template body argument parser.
|
|
||||||
pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> {
|
|
||||||
matches.get_one::<String>(ARG_BODY).map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents the raw template argument parser.
|
|
||||||
pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl {
|
|
||||||
matches
|
|
||||||
.get_one::<String>(ARG_TPL)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
|
@ -1,157 +0,0 @@
|
||||||
use anyhow::{anyhow, Result};
|
|
||||||
use atty::Stream;
|
|
||||||
use email::{
|
|
||||||
account::AccountConfig,
|
|
||||||
backend::Backend,
|
|
||||||
email::{Flag, Flags, Message},
|
|
||||||
sender::Sender,
|
|
||||||
};
|
|
||||||
use mml::MmlCompilerBuilder;
|
|
||||||
use std::io::{stdin, BufRead};
|
|
||||||
|
|
||||||
use crate::{printer::Printer, IdMapper};
|
|
||||||
|
|
||||||
pub async fn forward<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
id: &str,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids([id])?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let tpl: String = backend
|
|
||||||
.get_emails(folder, ids)
|
|
||||||
.await?
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
|
||||||
.to_forward_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.build()
|
|
||||||
.await?
|
|
||||||
.into();
|
|
||||||
|
|
||||||
printer.print(tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reply<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
id: &str,
|
|
||||||
all: bool,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let ids = id_mapper.get_ids([id])?;
|
|
||||||
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let tpl: String = backend
|
|
||||||
.get_emails(folder, ids)
|
|
||||||
.await?
|
|
||||||
.first()
|
|
||||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
|
||||||
.to_reply_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.with_reply_all(all)
|
|
||||||
.build()
|
|
||||||
.await?
|
|
||||||
.into();
|
|
||||||
|
|
||||||
printer.print(tpl)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn save<P: Printer>(
|
|
||||||
#[allow(unused_variables)] config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
id_mapper: &IdMapper,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
folder: &str,
|
|
||||||
tpl: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
|
||||||
tpl.replace("\r", "")
|
|
||||||
} else {
|
|
||||||
stdin()
|
|
||||||
.lock()
|
|
||||||
.lines()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let compiler = MmlCompilerBuilder::new();
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
let compiler = compiler.with_pgp(config.pgp.clone());
|
|
||||||
|
|
||||||
let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
|
|
||||||
|
|
||||||
let id = backend.add_email(folder, &email, &Flags::default()).await?;
|
|
||||||
id_mapper.create_alias(id)?;
|
|
||||||
|
|
||||||
printer.print("Template successfully saved!")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
backend: &mut dyn Backend,
|
|
||||||
sender: &mut dyn Sender,
|
|
||||||
tpl: String,
|
|
||||||
) -> Result<()> {
|
|
||||||
let folder = config.sent_folder_alias()?;
|
|
||||||
|
|
||||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
|
||||||
tpl.replace("\r", "")
|
|
||||||
} else {
|
|
||||||
stdin()
|
|
||||||
.lock()
|
|
||||||
.lines()
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
let compiler = MmlCompilerBuilder::new();
|
|
||||||
|
|
||||||
#[cfg(feature = "pgp")]
|
|
||||||
let compiler = compiler.with_pgp(config.pgp.clone());
|
|
||||||
|
|
||||||
let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
|
|
||||||
|
|
||||||
sender.send(&email).await?;
|
|
||||||
|
|
||||||
if config.email_sending_save_copy {
|
|
||||||
backend
|
|
||||||
.add_email(&folder, &email, &Flags::from_iter([Flag::Seen]))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
printer.print("Template successfully sent!")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn write<P: Printer>(
|
|
||||||
config: &AccountConfig,
|
|
||||||
printer: &mut P,
|
|
||||||
headers: Option<Vec<(&str, &str)>>,
|
|
||||||
body: Option<&str>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let tpl: String = Message::new_tpl_builder(config)
|
|
||||||
.with_some_headers(headers)
|
|
||||||
.with_some_body(body)
|
|
||||||
.build()
|
|
||||||
.await?
|
|
||||||
.into();
|
|
||||||
|
|
||||||
printer.print(tpl)
|
|
||||||
}
|
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod args;
|
|
||||||
pub mod handlers;
|
|
17
src/email/envelope/arg/ids.rs
Normal file
17
src/email/envelope/arg/ids.rs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The envelope id argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct EnvelopeIdArg {
|
||||||
|
/// The envelope id.
|
||||||
|
#[arg(value_name = "ID", required = true)]
|
||||||
|
pub id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The envelopes ids arguments parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct EnvelopeIdsArgs {
|
||||||
|
/// The list of envelopes ids.
|
||||||
|
#[arg(value_name = "ID", required = true)]
|
||||||
|
pub ids: Vec<usize>,
|
||||||
|
}
|
1
src/email/envelope/arg/mod.rs
Normal file
1
src/email/envelope/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod ids;
|
76
src/email/envelope/command/list.rs
Normal file
76
src/email/envelope/command/list.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
folder::arg::name::FolderNameOptionalArg,
|
||||||
|
printer::{PrintTableOpts, Printer},
|
||||||
|
ui::arg::max_width::TableMaxWidthFlag,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// List all envelopes.
|
||||||
|
///
|
||||||
|
/// This command allows you to list all envelopes included in the
|
||||||
|
/// given folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct EnvelopeListCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalArg,
|
||||||
|
|
||||||
|
/// The page number.
|
||||||
|
///
|
||||||
|
/// The page number starts from 1 (which is the default). Giving a
|
||||||
|
/// page number to big will result in a out of bound error.
|
||||||
|
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
|
||||||
|
pub page: usize,
|
||||||
|
|
||||||
|
/// The page size.
|
||||||
|
///
|
||||||
|
/// Determine the amount of envelopes a page should contain.
|
||||||
|
#[arg(long, short = 's', value_name = "NUMBER")]
|
||||||
|
pub page_size: Option<usize>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub table: TableMaxWidthFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeListCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing envelope list command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
|
||||||
|
let some_account_name = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let (toml_account_config, account_config) = config
|
||||||
|
.clone()
|
||||||
|
.into_account_configs(some_account_name, self.cache.disable)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let page_size = self
|
||||||
|
.page_size
|
||||||
|
.unwrap_or(account_config.get_envelope_list_page_size());
|
||||||
|
let page = 1.max(self.page) - 1;
|
||||||
|
|
||||||
|
let envelopes = backend.list_envelopes(folder, page_size, page).await?;
|
||||||
|
|
||||||
|
printer.print_table(
|
||||||
|
Box::new(envelopes),
|
||||||
|
PrintTableOpts {
|
||||||
|
format: &account_config.get_message_read_format(),
|
||||||
|
max_width: self.table.max_width,
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
28
src/email/envelope/command/mod.rs
Normal file
28
src/email/envelope/command/mod.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
pub mod list;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::{config::TomlConfig, printer::Printer};
|
||||||
|
|
||||||
|
use self::list::EnvelopeListCommand;
|
||||||
|
|
||||||
|
/// Manage envelopes.
|
||||||
|
///
|
||||||
|
/// An envelope is a small representation of a message. It contains an
|
||||||
|
/// identifier (given by the backend), some flags as well as few
|
||||||
|
/// headers from the message itself. This subcommand allows you to
|
||||||
|
/// manage them.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum EnvelopeSubcommand {
|
||||||
|
#[command(alias = "lst")]
|
||||||
|
List(EnvelopeListCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeSubcommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::List(cmd) => cmd.execute(printer, config).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
src/email/envelope/config.rs
Normal file
63
src/email/envelope/config.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::backend::BackendKind;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct EnvelopeConfig {
|
||||||
|
pub list: Option<EnvelopeListConfig>,
|
||||||
|
pub get: Option<EnvelopeGetConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(list) = &self.list {
|
||||||
|
kinds.extend(list.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(get) = &self.get {
|
||||||
|
kinds.extend(get.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct EnvelopeListConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub remote: email::envelope::list::config::EnvelopeListConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeListConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct EnvelopeGetConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EnvelopeGetConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
48
src/email/envelope/flag/arg/ids_and_flags.rs
Normal file
48
src/email/envelope/flag/arg/ids_and_flags.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use email::flag::{Flag, Flags};
|
||||||
|
use log::debug;
|
||||||
|
|
||||||
|
/// The ids and/or flags arguments parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct IdsAndFlagsArgs {
|
||||||
|
/// The list of ids and/or flags.
|
||||||
|
///
|
||||||
|
/// Every argument that can be parsed as an integer is considered
|
||||||
|
/// an id, otherwise it is considered as a flag.
|
||||||
|
#[arg(value_name = "ID-OR-FLAG", required = true)]
|
||||||
|
pub ids_and_flags: Vec<IdOrFlag>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum IdOrFlag {
|
||||||
|
Id(usize),
|
||||||
|
Flag(Flag),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for IdOrFlag {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
value.parse::<usize>().map(Self::Id).unwrap_or_else(|err| {
|
||||||
|
let flag = Flag::from(value);
|
||||||
|
debug!("cannot parse {value} as usize, parsing it as flag {flag}");
|
||||||
|
debug!("{err:?}");
|
||||||
|
Self::Flag(flag)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec<usize>, Flags) {
|
||||||
|
ids_and_flags.iter().fold(
|
||||||
|
(Vec::default(), Flags::default()),
|
||||||
|
|(mut ids, mut flags), arg| {
|
||||||
|
match arg {
|
||||||
|
IdOrFlag::Id(id) => {
|
||||||
|
ids.push(*id);
|
||||||
|
}
|
||||||
|
IdOrFlag::Flag(flag) => {
|
||||||
|
flags.insert(flag.to_owned());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(ids, flags)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
1
src/email/envelope/flag/arg/mod.rs
Normal file
1
src/email/envelope/flag/arg/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod ids_and_flags;
|
51
src/email/envelope/flag/command/add.rs
Normal file
51
src/email/envelope/flag/command/add.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Add flag(s) to an envelope.
|
||||||
|
///
|
||||||
|
/// This command allows you to attach the given flag(s) to the given
|
||||||
|
/// envelope(s).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct FlagAddCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub args: IdsAndFlagsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagAddCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing flag add command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||||
|
backend.add_flags(folder, &ids, &flags).await?;
|
||||||
|
|
||||||
|
printer.print(format!("Flag(s) {flags} successfully added!"))
|
||||||
|
}
|
||||||
|
}
|
41
src/email/envelope/flag/command/mod.rs
Normal file
41
src/email/envelope/flag/command/mod.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
mod add;
|
||||||
|
mod remove;
|
||||||
|
mod set;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::{config::TomlConfig, printer::Printer};
|
||||||
|
|
||||||
|
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
|
||||||
|
|
||||||
|
/// Manage flags.
|
||||||
|
///
|
||||||
|
/// A flag is a tag associated to an envelope. Existing flags are
|
||||||
|
/// seen, answered, flagged, deleted, draft. Other flags are
|
||||||
|
/// considered custom, which are not always supported (the
|
||||||
|
/// synchronization does not take care of them yet).
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum FlagSubcommand {
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(alias = "create")]
|
||||||
|
Add(FlagAddCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(aliases = ["update", "change", "replace"])]
|
||||||
|
Set(FlagSetCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(aliases = ["rm", "delete", "del"])]
|
||||||
|
Remove(FlagRemoveCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagSubcommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Add(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Set(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Remove(cmd) => cmd.execute(printer, config).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
src/email/envelope/flag/command/remove.rs
Normal file
51
src/email/envelope/flag/command/remove.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Remove flag(s) from an envelope.
|
||||||
|
///
|
||||||
|
/// This command allows you to remove the given flag(s) from the given
|
||||||
|
/// envelope(s).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct FlagRemoveCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub args: IdsAndFlagsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagRemoveCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing flag remove command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||||
|
backend.remove_flags(folder, &ids, &flags).await?;
|
||||||
|
|
||||||
|
printer.print(format!("Flag(s) {flags} successfully removed!"))
|
||||||
|
}
|
||||||
|
}
|
51
src/email/envelope/flag/command/set.rs
Normal file
51
src/email/envelope/flag/command/set.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Replace flag(s) of an envelope.
|
||||||
|
///
|
||||||
|
/// This command allows you to replace existing flags of the given
|
||||||
|
/// envelope(s) with the given flag(s).
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct FlagSetCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub args: IdsAndFlagsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagSetCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing flag set command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
|
||||||
|
backend.set_flags(folder, &ids, &flags).await?;
|
||||||
|
|
||||||
|
printer.print(format!("Flag(s) {flags} successfully set!"))
|
||||||
|
}
|
||||||
|
}
|
82
src/email/envelope/flag/config.rs
Normal file
82
src/email/envelope/flag/config.rs
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::backend::BackendKind;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct FlagConfig {
|
||||||
|
pub add: Option<FlagAddConfig>,
|
||||||
|
pub set: Option<FlagSetConfig>,
|
||||||
|
pub remove: Option<FlagRemoveConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(add) = &self.add {
|
||||||
|
kinds.extend(add.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(set) = &self.set {
|
||||||
|
kinds.extend(set.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(remove) = &self.remove {
|
||||||
|
kinds.extend(remove.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct FlagAddConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagAddConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct FlagSetConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagSetConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct FlagRemoveConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlagRemoveConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
48
src/email/envelope/flag/mod.rs
Normal file
48
src/email/envelope/flag/mod.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
pub mod arg;
|
||||||
|
pub mod command;
|
||||||
|
pub mod config;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
use std::{collections::HashSet, ops};
|
||||||
|
|
||||||
|
/// Represents the flag variants.
|
||||||
|
#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize)]
|
||||||
|
pub enum Flag {
|
||||||
|
Seen,
|
||||||
|
Answered,
|
||||||
|
Flagged,
|
||||||
|
Deleted,
|
||||||
|
Draft,
|
||||||
|
Custom(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&email::flag::Flag> for Flag {
|
||||||
|
fn from(flag: &email::flag::Flag) -> Self {
|
||||||
|
use email::flag::Flag::*;
|
||||||
|
match flag {
|
||||||
|
Seen => Flag::Seen,
|
||||||
|
Answered => Flag::Answered,
|
||||||
|
Flagged => Flag::Flagged,
|
||||||
|
Deleted => Flag::Deleted,
|
||||||
|
Draft => Flag::Draft,
|
||||||
|
Custom(flag) => Flag::Custom(flag.clone()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
|
||||||
|
pub struct Flags(pub HashSet<Flag>);
|
||||||
|
|
||||||
|
impl ops::Deref for Flags {
|
||||||
|
type Target = HashSet<Flag>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<email::flag::Flags> for Flags {
|
||||||
|
fn from(flags: email::flag::Flags) -> Self {
|
||||||
|
Flags(flags.iter().map(Flag::from).collect())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,80 @@
|
||||||
|
pub mod arg;
|
||||||
|
pub mod command;
|
||||||
|
pub mod config;
|
||||||
|
pub mod flag;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use email::account::AccountConfig;
|
use email::account::config::AccountConfig;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::ops;
|
use std::ops;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
cache::IdMapper,
|
||||||
|
flag::{Flag, Flags},
|
||||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||||
ui::Table,
|
ui::{Cell, Row, Table},
|
||||||
Envelope, IdMapper, Mailbox,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct Mailbox {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub addr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct Envelope {
|
||||||
|
pub id: String,
|
||||||
|
pub flags: Flags,
|
||||||
|
pub subject: String,
|
||||||
|
pub from: Mailbox,
|
||||||
|
pub date: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Table for Envelope {
|
||||||
|
fn head() -> Row {
|
||||||
|
Row::new()
|
||||||
|
.cell(Cell::new("ID").bold().underline().white())
|
||||||
|
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||||
|
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||||
|
.cell(Cell::new("FROM").bold().underline().white())
|
||||||
|
.cell(Cell::new("DATE").bold().underline().white())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn row(&self) -> Row {
|
||||||
|
let id = self.id.to_string();
|
||||||
|
let unseen = !self.flags.contains(&Flag::Seen);
|
||||||
|
let flags = {
|
||||||
|
let mut flags = String::new();
|
||||||
|
flags.push_str(if !unseen { " " } else { "✷" });
|
||||||
|
flags.push_str(if self.flags.contains(&Flag::Answered) {
|
||||||
|
"↵"
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
});
|
||||||
|
flags.push_str(if self.flags.contains(&Flag::Flagged) {
|
||||||
|
"⚑"
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
});
|
||||||
|
flags
|
||||||
|
};
|
||||||
|
let subject = &self.subject;
|
||||||
|
let sender = if let Some(name) = &self.from.name {
|
||||||
|
name
|
||||||
|
} else {
|
||||||
|
&self.from.addr
|
||||||
|
};
|
||||||
|
let date = &self.date;
|
||||||
|
|
||||||
|
Row::new()
|
||||||
|
.cell(Cell::new(id).bold_if(unseen).red())
|
||||||
|
.cell(Cell::new(flags).bold_if(unseen).white())
|
||||||
|
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
|
||||||
|
.cell(Cell::new(sender).bold_if(unseen).blue())
|
||||||
|
.cell(Cell::new(date).bold_if(unseen).yellow())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents the list of envelopes.
|
/// Represents the list of envelopes.
|
||||||
#[derive(Clone, Debug, Default, Serialize)]
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
pub struct Envelopes(Vec<Envelope>);
|
pub struct Envelopes(Vec<Envelope>);
|
||||||
|
@ -17,7 +83,7 @@ impl Envelopes {
|
||||||
pub fn from_backend(
|
pub fn from_backend(
|
||||||
config: &AccountConfig,
|
config: &AccountConfig,
|
||||||
id_mapper: &IdMapper,
|
id_mapper: &IdMapper,
|
||||||
envelopes: email::email::Envelopes,
|
envelopes: email::envelope::Envelopes,
|
||||||
) -> Result<Envelopes> {
|
) -> Result<Envelopes> {
|
||||||
let envelopes = envelopes
|
let envelopes = envelopes
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -59,17 +125,19 @@ impl PrintTable for Envelopes {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use email::account::AccountConfig;
|
use email::account::config::AccountConfig;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use crate::{Envelopes, IdMapper};
|
use crate::cache::IdMapper;
|
||||||
|
|
||||||
|
use super::Envelopes;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn default_datetime_fmt() {
|
fn default_datetime_fmt() {
|
||||||
let config = AccountConfig::default();
|
let config = AccountConfig::default();
|
||||||
let id_mapper = IdMapper::Dummy;
|
let id_mapper = IdMapper::Dummy;
|
||||||
|
|
||||||
let envelopes = email::email::Envelopes::from_iter([email::email::Envelope {
|
let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope {
|
||||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}]);
|
}]);
|
||||||
|
@ -89,7 +157,7 @@ mod tests {
|
||||||
..AccountConfig::default()
|
..AccountConfig::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let envelopes = email::email::Envelopes::from_iter([email::email::Envelope {
|
let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope {
|
||||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}]);
|
}]);
|
||||||
|
@ -112,7 +180,7 @@ mod tests {
|
||||||
..AccountConfig::default()
|
..AccountConfig::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let envelopes = email::email::Envelopes::from_iter([email::email::Envelope {
|
let envelopes = email::envelope::Envelopes::from_iter([email::envelope::Envelope {
|
||||||
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}]);
|
}]);
|
25
src/email/message/arg/body.rs
Normal file
25
src/email/message/arg/body.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
/// The raw message body argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageRawBodyArg {
|
||||||
|
/// Prefill the template with a custom body.
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
#[arg(name = "body_raw", value_name = "BODY")]
|
||||||
|
pub raw: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageRawBodyArg {
|
||||||
|
pub fn raw(self) -> String {
|
||||||
|
self.raw.join(" ").replace("\r", "").replace("\n", "\r\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for MessageRawBodyArg {
|
||||||
|
type Target = Vec<String>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.raw
|
||||||
|
}
|
||||||
|
}
|
20
src/email/message/arg/header.rs
Normal file
20
src/email/message/arg/header.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The envelope id argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct HeaderRawArgs {
|
||||||
|
/// Prefill the template with custom headers.
|
||||||
|
///
|
||||||
|
/// A raw header should follow the pattern KEY:VAL.
|
||||||
|
#[arg(long = "header", short = 'H', required = false)]
|
||||||
|
#[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)]
|
||||||
|
pub raw: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
|
||||||
|
if let Some((key, val)) = raw_header.split_once(":") {
|
||||||
|
Ok((key.trim().to_owned(), val.trim().to_owned()))
|
||||||
|
} else {
|
||||||
|
Err(format!("cannot parse raw header {raw_header:?}"))
|
||||||
|
}
|
||||||
|
}
|
20
src/email/message/arg/mod.rs
Normal file
20
src/email/message/arg/mod.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
pub mod body;
|
||||||
|
pub mod header;
|
||||||
|
pub mod reply;
|
||||||
|
|
||||||
|
/// The raw message argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageRawArg {
|
||||||
|
/// The raw message, including headers and body.
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
#[arg(name = "message_raw", value_name = "MESSAGE")]
|
||||||
|
pub raw: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageRawArg {
|
||||||
|
pub fn raw(self) -> String {
|
||||||
|
self.raw.join(" ").replace("\r", "").replace("\n", "\r\n")
|
||||||
|
}
|
||||||
|
}
|
12
src/email/message/arg/reply.rs
Normal file
12
src/email/message/arg/reply.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The reply to all argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageReplyAllArg {
|
||||||
|
/// Reply to all recipients.
|
||||||
|
///
|
||||||
|
/// This argument will add all recipients for the To and Cc
|
||||||
|
/// headers.
|
||||||
|
#[arg(long, short = 'A')]
|
||||||
|
pub all: bool,
|
||||||
|
}
|
89
src/email/message/attachment/command/download.rs
Normal file
89
src/email/message/attachment/command/download.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
use std::{fs, path::PathBuf};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Download all attachments for the given message.
|
||||||
|
///
|
||||||
|
/// This command allows you to download all attachments found for the
|
||||||
|
/// given message to your downloads directory.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct AttachmentDownloadCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelopes: EnvelopeIdsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttachmentDownloadCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing attachment download command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let ids = &self.envelopes.ids;
|
||||||
|
let emails = backend.get_messages(&folder, ids).await?;
|
||||||
|
|
||||||
|
let mut emails_count = 0;
|
||||||
|
let mut attachments_count = 0;
|
||||||
|
|
||||||
|
let mut ids = ids.iter();
|
||||||
|
for email in emails.to_vec() {
|
||||||
|
let id = ids.next().unwrap();
|
||||||
|
let attachments = email.attachments()?;
|
||||||
|
|
||||||
|
if attachments.is_empty() {
|
||||||
|
printer.print_log(format!("No attachment found for message {id}!"))?;
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
emails_count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.print_log(format!(
|
||||||
|
"{} attachment(s) found for message {id}!",
|
||||||
|
attachments.len()
|
||||||
|
))?;
|
||||||
|
|
||||||
|
for attachment in attachments {
|
||||||
|
let filename: PathBuf = attachment
|
||||||
|
.filename
|
||||||
|
.unwrap_or_else(|| Uuid::new_v4().to_string())
|
||||||
|
.into();
|
||||||
|
let filepath = account_config.get_download_file_path(&filename)?;
|
||||||
|
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
||||||
|
fs::write(&filepath, &attachment.body)
|
||||||
|
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
|
||||||
|
attachments_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match attachments_count {
|
||||||
|
0 => printer.print("No attachment found!"),
|
||||||
|
1 => printer.print("Downloaded 1 attachment!"),
|
||||||
|
n => printer.print(format!(
|
||||||
|
"Downloaded {} attachment(s) from {} messages(s)!",
|
||||||
|
n, emails_count,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/email/message/attachment/command/mod.rs
Normal file
27
src/email/message/attachment/command/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
pub mod download;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::{config::TomlConfig, printer::Printer};
|
||||||
|
|
||||||
|
use self::download::AttachmentDownloadCommand;
|
||||||
|
|
||||||
|
/// Manage attachments.
|
||||||
|
///
|
||||||
|
/// A message body can be composed of multiple MIME parts. An
|
||||||
|
/// attachment is the representation of a binary part of a message
|
||||||
|
/// body.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum AttachmentSubcommand {
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Download(AttachmentDownloadCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttachmentSubcommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Download(cmd) => cmd.execute(printer, config).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
src/email/message/attachment/mod.rs
Normal file
1
src/email/message/attachment/mod.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
pub mod command;
|
54
src/email/message/command/copy.rs
Normal file
54
src/email/message/command/copy.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
envelope::arg::ids::EnvelopeIdsArgs,
|
||||||
|
folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg},
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Copy a message from a source folder to a target folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageCopyCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub source_folder: SourceFolderNameArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub target_folder: TargetFolderNameArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelopes: EnvelopeIdsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageCopyCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message copy command");
|
||||||
|
|
||||||
|
let from_folder = &self.source_folder.name;
|
||||||
|
let to_folder = &self.target_folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let ids = &self.envelopes.ids;
|
||||||
|
backend.copy_messages(from_folder, to_folder, ids).await?;
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Message(s) successfully copied from {from_folder} to {to_folder}!"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
49
src/email/message/command/delete.rs
Normal file
49
src/email/message/command/delete.rs
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Mark as deleted a message from a folder.
|
||||||
|
///
|
||||||
|
/// This command does not really delete the message: if the given
|
||||||
|
/// folder points to the trash folder, it adds the "deleted" flag to
|
||||||
|
/// its envelope, otherwise it moves it to the trash folder. Only the
|
||||||
|
/// expunge folder command truly deletes messages.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageDeleteCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelopes: EnvelopeIdsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageDeleteCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message delete command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let ids = &self.envelopes.ids;
|
||||||
|
backend.delete_messages(folder, ids).await?;
|
||||||
|
|
||||||
|
printer.print("Message(s) successfully deleted from {from_folder} to {to_folder}!")
|
||||||
|
}
|
||||||
|
}
|
69
src/email/message/command/forward.rs
Normal file
69
src/email/message/command/forward.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
envelope::arg::ids::EnvelopeIdArg,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||||
|
printer::Printer,
|
||||||
|
ui::editor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Forward a message.
|
||||||
|
///
|
||||||
|
/// This command allows you to forward the given message using the
|
||||||
|
/// editor defined in your environment variable $EDITOR. When the
|
||||||
|
/// edition process finishes, you can choose between saving or sending
|
||||||
|
/// the final message.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageForwardCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelope: EnvelopeIdArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub headers: HeaderRawArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub body: MessageRawBodyArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageForwardCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message forward command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
|
||||||
|
let id = self.envelope.id;
|
||||||
|
let tpl = backend
|
||||||
|
.get_messages(folder, &[id])
|
||||||
|
.await?
|
||||||
|
.first()
|
||||||
|
.ok_or(anyhow!("cannot find message"))?
|
||||||
|
.to_forward_tpl_builder(&account_config)
|
||||||
|
.with_headers(self.headers.raw)
|
||||||
|
.with_body(self.body.raw())
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||||
|
}
|
||||||
|
}
|
81
src/email/message/command/mailto.rs
Normal file
81
src/email/message/command/mailto.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::{debug, info};
|
||||||
|
use mail_builder::MessageBuilder;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, printer::Printer, ui::editor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parse and edit a message from a mailto URL string.
|
||||||
|
///
|
||||||
|
/// This command allows you to edit a message from the mailto format
|
||||||
|
/// using the editor defined in your environment variable
|
||||||
|
/// $EDITOR. When the edition process finishes, you can choose between
|
||||||
|
/// saving or sending the final message.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageMailtoCommand {
|
||||||
|
/// The mailto url.
|
||||||
|
#[arg()]
|
||||||
|
pub url: Url,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageMailtoCommand {
|
||||||
|
pub fn new(url: &str) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
url: Url::parse(url)?,
|
||||||
|
cache: Default::default(),
|
||||||
|
account: Default::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message mailto command");
|
||||||
|
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
|
||||||
|
let mut builder = MessageBuilder::new().to(self.url.path());
|
||||||
|
let mut body = String::new();
|
||||||
|
|
||||||
|
for (key, val) in self.url.query_pairs() {
|
||||||
|
match key.to_lowercase().as_bytes() {
|
||||||
|
b"cc" => builder = builder.cc(val.to_string()),
|
||||||
|
b"bcc" => builder = builder.bcc(val.to_string()),
|
||||||
|
b"subject" => builder = builder.subject(val),
|
||||||
|
b"body" => body += &val,
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match account_config.find_full_signature() {
|
||||||
|
Ok(Some(ref signature)) => builder = builder.text_body(body + "\n\n" + signature),
|
||||||
|
Ok(None) => builder = builder.text_body(body),
|
||||||
|
Err(err) => {
|
||||||
|
debug!("cannot add signature to mailto message, skipping it: {err}");
|
||||||
|
debug!("{err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let tpl = account_config
|
||||||
|
.generate_tpl_interpreter()
|
||||||
|
.with_show_only_headers(account_config.get_message_write_headers())
|
||||||
|
.build()
|
||||||
|
.from_msg_builder(builder)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||||
|
}
|
||||||
|
}
|
81
src/email/message/command/mod.rs
Normal file
81
src/email/message/command/mod.rs
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
pub mod copy;
|
||||||
|
pub mod delete;
|
||||||
|
pub mod forward;
|
||||||
|
pub mod mailto;
|
||||||
|
pub mod move_;
|
||||||
|
pub mod read;
|
||||||
|
pub mod reply;
|
||||||
|
pub mod save;
|
||||||
|
pub mod send;
|
||||||
|
pub mod write;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Subcommand;
|
||||||
|
|
||||||
|
use crate::{config::TomlConfig, printer::Printer};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
|
||||||
|
mailto::MessageMailtoCommand, move_::MessageMoveCommand, read::MessageReadCommand,
|
||||||
|
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
|
||||||
|
write::MessageWriteCommand,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Manage messages.
|
||||||
|
///
|
||||||
|
/// A message is the content of an email. It is composed of headers
|
||||||
|
/// (located at the top of the message) and a body (located at the
|
||||||
|
/// bottom of the message). Both are separated by two new lines. This
|
||||||
|
/// subcommand allows you to manage them.
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum MessageSubcommand {
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Read(MessageReadCommand),
|
||||||
|
|
||||||
|
#[command(aliases = ["add", "create", "new", "compose"])]
|
||||||
|
Write(MessageWriteCommand),
|
||||||
|
|
||||||
|
#[command()]
|
||||||
|
Reply(MessageReplyCommand),
|
||||||
|
|
||||||
|
#[command(aliases = ["fwd", "fd"])]
|
||||||
|
Forward(MessageForwardCommand),
|
||||||
|
|
||||||
|
#[command()]
|
||||||
|
Mailto(MessageMailtoCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Save(MessageSaveCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
Send(MessageSendCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(aliases = ["cpy", "cp"])]
|
||||||
|
Copy(MessageCopyCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(alias = "mv")]
|
||||||
|
Move(MessageMoveCommand),
|
||||||
|
|
||||||
|
#[command(arg_required_else_help = true)]
|
||||||
|
#[command(aliases = ["remove", "rm"])]
|
||||||
|
Delete(MessageDeleteCommand),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSubcommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Read(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Write(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Reply(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Forward(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Mailto(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Save(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Send(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Copy(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Move(cmd) => cmd.execute(printer, config).await,
|
||||||
|
Self::Delete(cmd) => cmd.execute(printer, config).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
src/email/message/command/move_.rs
Normal file
54
src/email/message/command/move_.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
envelope::arg::ids::EnvelopeIdsArgs,
|
||||||
|
folder::arg::name::{SourceFolderNameArg, TargetFolderNameArg},
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Move a message from a source folder to a target folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageMoveCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub source_folder: SourceFolderNameArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub target_folder: TargetFolderNameArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelopes: EnvelopeIdsArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageMoveCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message move command");
|
||||||
|
|
||||||
|
let from_folder = &self.source_folder.name;
|
||||||
|
let to_folder = &self.target_folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let ids = &self.envelopes.ids;
|
||||||
|
backend.move_messages(from_folder, to_folder, ids).await?;
|
||||||
|
|
||||||
|
printer.print(format!(
|
||||||
|
"Message(s) successfully moved from {from_folder} to {to_folder}!"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
130
src/email/message/command/read.rs
Normal file
130
src/email/message/command/read.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
use mml::message::FilterParts;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Read a message.
|
||||||
|
///
|
||||||
|
/// This command allows you to read a message. When reading a message,
|
||||||
|
/// the "seen" flag is automatically applied to the corresponding
|
||||||
|
/// envelope. To prevent this behaviour, use the --preview flag.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageReadCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelopes: EnvelopeIdsArgs,
|
||||||
|
|
||||||
|
/// Read the message without applying the "seen" flag to its
|
||||||
|
/// corresponding envelope.
|
||||||
|
#[arg(long, short)]
|
||||||
|
pub preview: bool,
|
||||||
|
|
||||||
|
/// Read the raw version of the given message.
|
||||||
|
///
|
||||||
|
/// The raw message represents the headers and the body as it is
|
||||||
|
/// on the backend, unedited: not decoded nor decrypted. This is
|
||||||
|
/// useful for debugging faulty messages, but also for
|
||||||
|
/// saving/sending/transfering messages.
|
||||||
|
#[arg(long, short)]
|
||||||
|
#[arg(conflicts_with = "no_headers")]
|
||||||
|
#[arg(conflicts_with = "headers")]
|
||||||
|
pub raw: bool,
|
||||||
|
|
||||||
|
/// Read only body of text/html parts.
|
||||||
|
///
|
||||||
|
/// This argument is useful when you need to read the HTML version
|
||||||
|
/// of a message. Combined with --no-headers, you can write it to
|
||||||
|
/// a .html file and open it with your favourite browser.
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(conflicts_with = "raw")]
|
||||||
|
pub html: bool,
|
||||||
|
|
||||||
|
/// Read only the body of the message.
|
||||||
|
///
|
||||||
|
/// All headers will be removed from the message.
|
||||||
|
#[arg(long)]
|
||||||
|
#[arg(conflicts_with = "raw")]
|
||||||
|
#[arg(conflicts_with = "headers")]
|
||||||
|
pub no_headers: bool,
|
||||||
|
|
||||||
|
/// List of headers that should be visible at the top of the
|
||||||
|
/// message.
|
||||||
|
///
|
||||||
|
/// If a given header is not found in the message, it will not be
|
||||||
|
/// visible. If no header is given, defaults to the one set up in
|
||||||
|
/// your TOML configuration file.
|
||||||
|
#[arg(long = "header", short = 'H', value_name = "NAME")]
|
||||||
|
#[arg(conflicts_with = "raw")]
|
||||||
|
#[arg(conflicts_with = "no_headers")]
|
||||||
|
pub headers: Vec<String>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageReadCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message read command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let ids = &self.envelopes.ids;
|
||||||
|
let emails = if self.preview {
|
||||||
|
backend.peek_messages(&folder, &ids).await
|
||||||
|
} else {
|
||||||
|
backend.get_messages(&folder, &ids).await
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let mut glue = "";
|
||||||
|
let mut bodies = String::default();
|
||||||
|
|
||||||
|
for email in emails.to_vec() {
|
||||||
|
bodies.push_str(glue);
|
||||||
|
|
||||||
|
if self.raw {
|
||||||
|
// emails do not always have valid utf8, uses "lossy" to
|
||||||
|
// display what can be displayed
|
||||||
|
bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned());
|
||||||
|
} else {
|
||||||
|
let tpl: String = email
|
||||||
|
.to_read_tpl(&account_config, |mut tpl| {
|
||||||
|
if self.no_headers {
|
||||||
|
tpl = tpl.with_hide_all_headers();
|
||||||
|
} else if !self.headers.is_empty() {
|
||||||
|
tpl = tpl.with_show_only_headers(&self.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.html {
|
||||||
|
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
.into();
|
||||||
|
bodies.push_str(&tpl);
|
||||||
|
}
|
||||||
|
|
||||||
|
glue = "\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
printer.print(bodies)
|
||||||
|
}
|
||||||
|
}
|
75
src/email/message/command/reply.rs
Normal file
75
src/email/message/command/reply.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use email::flag::Flag;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
envelope::arg::ids::EnvelopeIdArg,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
|
||||||
|
printer::Printer,
|
||||||
|
ui::editor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Reply to a message.
|
||||||
|
///
|
||||||
|
/// This command allows you to reply to the given message using the
|
||||||
|
/// editor defined in your environment variable $EDITOR. When the
|
||||||
|
/// edition process finishes, you can choose between saving or sending
|
||||||
|
/// the final message.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageReplyCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelope: EnvelopeIdArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub reply: MessageReplyAllArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub headers: HeaderRawArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub body: MessageRawBodyArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageReplyCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message reply command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
|
||||||
|
let id = self.envelope.id;
|
||||||
|
let tpl = backend
|
||||||
|
.get_messages(folder, &[id])
|
||||||
|
.await?
|
||||||
|
.first()
|
||||||
|
.ok_or(anyhow!("cannot find message {id}"))?
|
||||||
|
.to_reply_tpl_builder(&account_config)
|
||||||
|
.with_headers(self.headers.raw)
|
||||||
|
.with_body(self.body.raw())
|
||||||
|
.with_reply_all(self.reply.all)
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await?;
|
||||||
|
backend.add_flag(&folder, &[id], Flag::Answered).await
|
||||||
|
}
|
||||||
|
}
|
59
src/email/message/command/save.rs
Normal file
59
src/email/message/command/save.rs
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
use std::io::{self, BufRead, IsTerminal};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Save a message to a folder.
|
||||||
|
///
|
||||||
|
/// This command allows you to add a raw message to the given folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageSaveCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub message: MessageRawArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSaveCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message save command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
|
||||||
|
let is_tty = io::stdin().is_terminal();
|
||||||
|
let is_json = printer.is_json();
|
||||||
|
let msg = if is_tty || is_json {
|
||||||
|
self.message.raw()
|
||||||
|
} else {
|
||||||
|
io::stdin()
|
||||||
|
.lock()
|
||||||
|
.lines()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\r\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
backend.add_raw_message(folder, msg.as_bytes()).await?;
|
||||||
|
|
||||||
|
printer.print(format!("Message successfully saved to {folder}!"))
|
||||||
|
}
|
||||||
|
}
|
65
src/email/message/command/send.rs
Normal file
65
src/email/message/command/send.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use email::flag::Flag;
|
||||||
|
use log::info;
|
||||||
|
use std::io::{self, BufRead, IsTerminal};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag, backend::Backend, cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig, message::arg::MessageRawArg, printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Send a message.
|
||||||
|
///
|
||||||
|
/// This command allows you to send a raw message and to save a copy
|
||||||
|
/// to your send folder.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageSendCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub message: MessageRawArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSendCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message send command");
|
||||||
|
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
let folder = account_config.get_sent_folder_alias()?;
|
||||||
|
|
||||||
|
let is_tty = io::stdin().is_terminal();
|
||||||
|
let is_json = printer.is_json();
|
||||||
|
let msg = if is_tty || is_json {
|
||||||
|
self.message.raw()
|
||||||
|
} else {
|
||||||
|
io::stdin()
|
||||||
|
.lock()
|
||||||
|
.lines()
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\r\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
backend.send_raw_message(msg.as_bytes()).await?;
|
||||||
|
|
||||||
|
if account_config.should_save_copy_sent_message() {
|
||||||
|
backend
|
||||||
|
.add_raw_message_with_flag(&folder, msg.as_bytes(), Flag::Seen)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
printer.print(format!("Message successfully sent and saved to {folder}!"))
|
||||||
|
} else {
|
||||||
|
printer.print("Message successfully sent!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/email/message/command/write.rs
Normal file
56
src/email/message/command/write.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::Parser;
|
||||||
|
use email::message::Message;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||||
|
printer::Printer,
|
||||||
|
ui::editor,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Write a new message.
|
||||||
|
///
|
||||||
|
/// This command allows you to write a new message using the editor
|
||||||
|
/// defined in your environment variable $EDITOR. When the edition
|
||||||
|
/// process finishes, you can choose between saving or sending the
|
||||||
|
/// final message.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct MessageWriteCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub headers: HeaderRawArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub body: MessageRawBodyArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageWriteCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing message write command");
|
||||||
|
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), true).await?;
|
||||||
|
|
||||||
|
let tpl = Message::new_tpl_builder(&account_config)
|
||||||
|
.with_headers(self.headers.raw)
|
||||||
|
.with_body(self.body.raw())
|
||||||
|
.build()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
editor::edit_tpl_with_editor(&account_config, printer, &backend, tpl).await
|
||||||
|
}
|
||||||
|
}
|
158
src/email/message/config.rs
Normal file
158
src/email/message/config.rs
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use crate::backend::BackendKind;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageConfig {
|
||||||
|
pub write: Option<MessageAddConfig>,
|
||||||
|
pub send: Option<MessageSendConfig>,
|
||||||
|
pub peek: Option<MessagePeekConfig>,
|
||||||
|
pub read: Option<MessageGetConfig>,
|
||||||
|
pub copy: Option<MessageCopyConfig>,
|
||||||
|
#[serde(rename = "move")]
|
||||||
|
pub move_: Option<MessageMoveConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(add) = &self.write {
|
||||||
|
kinds.extend(add.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(send) = &self.send {
|
||||||
|
kinds.extend(send.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(peek) = &self.peek {
|
||||||
|
kinds.extend(peek.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(get) = &self.read {
|
||||||
|
kinds.extend(get.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(copy) = &self.copy {
|
||||||
|
kinds.extend(copy.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(move_) = &self.move_ {
|
||||||
|
kinds.extend(move_.get_used_backends());
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageAddConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub remote: email::message::add_raw::config::MessageWriteConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageAddConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageSendConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub remote: email::message::send_raw::config::MessageSendConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageSendConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessagePeekConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessagePeekConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageGetConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub remote: email::message::get::config::MessageReadConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageGetConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageCopyConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageCopyConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct MessageMoveConfig {
|
||||||
|
pub backend: Option<BackendKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageMoveConfig {
|
||||||
|
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
|
||||||
|
let mut kinds = HashSet::default();
|
||||||
|
|
||||||
|
if let Some(kind) = &self.backend {
|
||||||
|
kinds.insert(kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
kinds
|
||||||
|
}
|
||||||
|
}
|
5
src/email/message/mod.rs
Normal file
5
src/email/message/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod arg;
|
||||||
|
pub mod attachment;
|
||||||
|
pub mod command;
|
||||||
|
pub mod config;
|
||||||
|
pub mod template;
|
25
src/email/message/template/arg/body.rs
Normal file
25
src/email/message/template/arg/body.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use clap::Parser;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
/// The raw template body argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct TemplateRawBodyArg {
|
||||||
|
/// Prefill the template with a custom MML body.
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
#[arg(name = "body_raw", value_name = "BODY")]
|
||||||
|
pub raw: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateRawBodyArg {
|
||||||
|
pub fn raw(self) -> String {
|
||||||
|
self.raw.join(" ").replace("\r", "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for TemplateRawBodyArg {
|
||||||
|
type Target = Vec<String>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.raw
|
||||||
|
}
|
||||||
|
}
|
18
src/email/message/template/arg/mod.rs
Normal file
18
src/email/message/template/arg/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
pub mod body;
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
/// The raw template argument parser.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct TemplateRawArg {
|
||||||
|
/// The raw template, including headers and MML body.
|
||||||
|
#[arg(trailing_var_arg = true)]
|
||||||
|
#[arg(name = "template_raw", value_name = "TEMPLATE")]
|
||||||
|
pub raw: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateRawArg {
|
||||||
|
pub fn raw(self) -> String {
|
||||||
|
self.raw.join(" ").replace("\r", "")
|
||||||
|
}
|
||||||
|
}
|
69
src/email/message/template/command/forward.rs
Normal file
69
src/email/message/template/command/forward.rs
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
account::arg::name::AccountNameFlag,
|
||||||
|
backend::Backend,
|
||||||
|
cache::arg::disable::CacheDisableFlag,
|
||||||
|
config::TomlConfig,
|
||||||
|
envelope::arg::ids::EnvelopeIdArg,
|
||||||
|
folder::arg::name::FolderNameOptionalFlag,
|
||||||
|
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
|
||||||
|
printer::Printer,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generate a template for forwarding a message.
|
||||||
|
///
|
||||||
|
/// The generated template is prefilled with your email in a From
|
||||||
|
/// header as well as your signature. The forwarded message is also
|
||||||
|
/// prefilled in the body of the template, prefixed by a separator.
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct TemplateForwardCommand {
|
||||||
|
#[command(flatten)]
|
||||||
|
pub folder: FolderNameOptionalFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub envelope: EnvelopeIdArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub headers: HeaderRawArgs,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub body: MessageRawBodyArg,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub cache: CacheDisableFlag,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub account: AccountNameFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TemplateForwardCommand {
|
||||||
|
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
|
||||||
|
info!("executing template forward command");
|
||||||
|
|
||||||
|
let folder = &self.folder.name;
|
||||||
|
let account = self.account.name.as_ref().map(String::as_str);
|
||||||
|
let cache = self.cache.disable;
|
||||||
|
|
||||||
|
let (toml_account_config, account_config) =
|
||||||
|
config.clone().into_account_configs(account, cache)?;
|
||||||
|
let backend = Backend::new(toml_account_config, account_config.clone(), false).await?;
|
||||||
|
|
||||||
|
let id = self.envelope.id;
|
||||||
|
let tpl: String = backend
|
||||||
|
.get_messages(folder, &[id])
|
||||||
|
.await?
|
||||||
|
.first()
|
||||||
|
.ok_or(anyhow!("cannot find message {id}"))?
|
||||||
|
.to_forward_tpl_builder(&account_config)
|
||||||
|
.with_headers(self.headers.raw)
|
||||||
|
.with_body(self.body.raw())
|
||||||
|
.build()
|
||||||
|
.await?
|
||||||
|
.into();
|
||||||
|
|
||||||
|
printer.print(tpl)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue