Merge branch 'pimalaya-tui-refactor'

This commit is contained in:
Clément DOUIN 2024-10-28 11:29:37 +01:00
commit 6ff3771135
No known key found for this signature in database
GPG key ID: 353E4A18EE0FAB72
73 changed files with 1403 additions and 4930 deletions

View file

@ -30,9 +30,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Refactored IMAP and SMTP auth config API - Improved error messages when missing cargo features. For example, if a TOML configuration uses the IMAP backend without the `imap` cargo features, the error `missing "imap" feature` is displayed. [#20](https://github.com/pimalaya/core/issues/20)
- Normalized enum-based configurations, using the [internally tagged representation](https://serde.rs/enum-representations.html#internally-tagged) `type =`. It should reduce issues due to misconfiguration, and improve othe error messages. Yet it is not perfect, see [#802](https://github.com/toml-rs/toml/issues/802):
The IMAP and SMTP auth config option is now explicit, in order to improve error messages: - `imap.*`, `maildir.*` and `notmuch.*` moved to `backend.*`:
```toml
# before
imap.host = "localhost"
imap.port = 143
# after
backend.type = "imap"
backend.host = "localhost"
backend.port = 143
```
- `smtp.*` and `sendmail.*` moved to `message.send.backend.*`:
```toml
# before
smtp.host = "localhost"
smtp.port = 25
# after
message.send.backend.type = "smtp"
message.send.backend.host = "localhost"
message.send.backend.port = 25
```
- `pgp.backend` renamed `pgp.type`:
```toml
# before
pgp.backend = "commands"
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
# after
pgp.type = "commands"
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
```
- `{imap,smtp}.auth` moved as well:
```toml ```toml
# before # before
@ -40,10 +79,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
smtp.oauth2.method = "xoauth2" smtp.oauth2.method = "xoauth2"
# after # after
imap.auth.type = "password" backend.auth.type = "password"
imap.auth.cmd = "pass show example" backend.auth.cmd = "pass show example"
smtp.auth.type = "oauth2" message.send.backend.auth.type = "oauth2"
smtp.auth.method = "xoauth2" message.send.backend.auth.method = "xoauth2"
``` ```
## [1.0.0-beta.4] - 2024-04-16 ## [1.0.0-beta.4] - 2024-04-16
@ -849,18 +888,3 @@ Few major concepts changed:
[0.2.1]: https://github.com/soywod/himalaya/compare/v0.2.0...v0.2.1 [0.2.1]: https://github.com/soywod/himalaya/compare/v0.2.0...v0.2.1
[0.2.0]: https://github.com/soywod/himalaya/compare/v0.1.0...v0.2.0 [0.2.0]: https://github.com/soywod/himalaya/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/soywod/himalaya/releases/tag/v0.1.0 [0.1.0]: https://github.com/soywod/himalaya/releases/tag/v0.1.0
[#39]: https://todo.sr.ht/~soywod/pimalaya/39
[#41]: https://todo.sr.ht/~soywod/pimalaya/41
[#43]: https://todo.sr.ht/~soywod/pimalaya/43
[#54]: https://todo.sr.ht/~soywod/pimalaya/54
[#58]: https://todo.sr.ht/~soywod/pimalaya/58
[#59]: https://todo.sr.ht/~soywod/pimalaya/59
[#60]: https://todo.sr.ht/~soywod/pimalaya/60
[#95]: https://todo.sr.ht/~soywod/pimalaya/95
[#172]: https://todo.sr.ht/~soywod/pimalaya/172
[#173]: https://todo.sr.ht/~soywod/pimalaya/173
[#184]: https://todo.sr.ht/~soywod/pimalaya/184
[#188]: https://todo.sr.ht/~soywod/pimalaya/188
[#194]: https://todo.sr.ht/~soywod/pimalaya/194
[#195]: https://todo.sr.ht/~soywod/pimalaya/195

1268
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ documentation = "https://github.com/pimalaya/himalaya/"
repository = "https://github.com/pimalaya/himalaya/" repository = "https://github.com/pimalaya/himalaya/"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs", "--document-private-items"] rustdoc-args = ["--cfg", "docsrs"]
[features] [features]
default = [ default = [
@ -21,12 +21,10 @@ default = [
"maildir", "maildir",
#"notmuch", #"notmuch",
"smtp", "smtp",
"sendmail", #"sendmail",
#"keyring", #"keyring",
#"oauth2", #"oauth2",
"wizard", "wizard",
#"pgp-commands", #"pgp-commands",
#"pgp-gpg", #"pgp-gpg",
#"pgp-native", #"pgp-native",
@ -38,57 +36,32 @@ notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"] smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"] sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib?/keyring-tokio"] keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"] oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["dep:email_address", "dep:secret-lib", "email-lib/autoconfig"] wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
pgp = [] pgp = []
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"] pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands", "pgp"]
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pgp"] pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg", "pgp"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pgp"] pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native", "pgp"]
[dependencies] [dependencies]
ariadne = "0.2" ariadne = "0.2"
async-trait = "0.1"
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4" clap_complete = "4.4"
clap_mangen = "0.2" clap_mangen = "0.2"
color-eyre = "0.6" color-eyre = "0.6"
comfy-table = "7.1" email-lib = { version = "=0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
crossterm = { version = "0.27", features = ["serde"] }
dirs = "4"
email-lib = { version = "=0.25.0", default-features = false, features = ["derive", "thread", "tracing"] }
email_address = { version = "0.2", optional = true }
mail-builder = "0.3" mail-builder = "0.3"
md5 = "0.7" mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"] }
oauth-lib = { version = "=0.1.1", optional = true }
once_cell = "1.16" once_cell = "1.16"
petgraph = "0.6" pimalaya-tui = { version = "=0.1", default-features = false, features = ["email", "path", "cli", "himalaya", "tracing", "sled"] }
pimalaya-tui = { version = "=0.1.0", default-features = false, features = ["email", "path", "cli", "config", "tracing"] } secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
shellexpand-utils = "=0.2.1" shellexpand-utils = "=0.2.1"
sled = "=0.34.7"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8" toml = "0.8"
tracing = "0.1" tracing = "0.1"
url = "2.2" url = "2.2"
uuid = { version = "0.8", features = ["v4"] } uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
# IMAP
imap-next = { git = "https://github.com/duesee/imap-next" }
imap-client = { git = "https://github.com/pimalaya/imap-client" }
# Pimalaya core
email-lib = { git = "https://github.com/pimalaya/core" }
keyring-lib = { git = "https://github.com/pimalaya/core" }
oauth-lib = { git = "https://github.com/pimalaya/core" }
process-lib = { git = "https://github.com/pimalaya/core" }
secret-lib = { git = "https://github.com/pimalaya/core" }
# Pimalaya TUI
pimalaya-tui = { git = "https://github.com/pimalaya/tui" }

View file

@ -621,12 +621,13 @@ You can also manually edit your own configuration, from scratch:
## Sponsoring ## Sponsoring
[![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/)
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: Special thanks to the [NLnet foundation](https://nlnet.nl/) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from various programs:
- [NGI Assure](https://nlnet.nl/assure/) in 2022 - [NGI Assure](https://nlnet.nl/project/Himalaya/) in 2022
- [NGI Zero Entrust](https://nlnet.nl/entrust/) in 2023 - [NGI Zero Entrust](https://nlnet.nl/project/Pimalaya/) in 2023
- [NGI Zero Core](https://nlnet.nl/project/Pimalaya-PIM/) in 2024 *(still ongoing)*
If you appreciate the project, feel free to donate using one of the following providers: If you appreciate the project, feel free to donate using one of the following providers:

View file

@ -288,13 +288,80 @@ template.forward.signature-style = "inlined"
template.forward.quote-headline = "-------- Forwarded Message --------\n" template.forward.quote-headline = "-------- Forwarded Message --------\n"
######################################## ########################################
#### PGP configuration ################# #### GPG-based PGP configuration #######
######################################## ########################################
# TODO # Enables PGP using GPG bindings. It requires the GPG lib to be
#pgp.backend = "commands" # installed on the system, and the `pgp-gpg` cargo feature on.
#pgp.backend = "gpg" #
#pgp.backend = "native" #pgp.type = "gpg"
########################################
#### Command-based PGP configuration ###
########################################
# Enables PGP using shell commands. A PGP client needs to be installed
# on the system, like gpg. It also requires the `pgp-commands` cargo
# feature.
#
#pgp.type = "commands"
# Defines the encrypt command. The special placeholder `<recipients>`
# represents the list of recipients, formatted by
# `pgp.encrypt-recipient-fmt`.
#
#pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
# Formats recipients for `pgp.encrypt-cmd`. The special placeholder
# `<recipient>` is replaced by an actual recipient at runtime.
#
#pgp.encrypt-recipient-fmt = "--recipient <recipient>"
# Defines the separator used between formatted recipients
# `pgp.encrypt-recipient-fmt`.
#
#pgp.encrypt-recipients-sep = " "
# Defines the decrypt command.
#
#pgp.decrypt-cmd = "gpg --decrypt --quiet"
# Defines the sign command.
#
#pgp.sign-cmd = "gpg --sign --quiet --armor"
# Defines the verify command.
#
#pgp.verify-cmd = "gpg --verify --quiet"
########################################
#### Native PGP configuration ##########
########################################
# Enables the native Rust implementation of PGP. It requires the
# `pgp-native` cargo feature.
#
#pgp.type = "native"
# Defines where to find the PGP secret key.
#
#pgp.secret-key.path = "/path/to/secret.key"
#pgp.secret-key.keyring = "my-pgp-secret-key"
# Defines how to retrieve the PGP secret key passphrase.
#
#pgp.secret-key-passphrase.raw = "p@assw0rd"
#pgp.secret-key-passphrase.keyring = "my-pgp-passphrase"
#pgp.secret-key-passphrase.cmd = "pass show pgp-passphrase"
# Enables the Web Key Discovery protocol to discover recipients'
# public key based on their email address.
#
#pgp.wkd = true
# Enables public key servers discovery.
#
#pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"]
######################################## ########################################
#### IMAP configuration ################ #### IMAP configuration ################
@ -302,54 +369,54 @@ template.forward.quote-headline = "-------- Forwarded Message --------\n"
# Defines the IMAP backend as the default one for all features. # Defines the IMAP backend as the default one for all features.
# #
backend = "imap" backend.type = "imap"
# IMAP server host name. # IMAP server host name.
# #
imap.host = "localhost" backend.host = "localhost"
# IMAP server port. # IMAP server port.
# #
#imap.port = 143 #backend.port = 143
imap.port = 993 backend.port = 993
# IMAP server encryption. # IMAP server encryption.
# #
#imap.encryption = "none" # or false #backend.encryption = "none" # or false
#imap.encryption = "start-tls" #backend.encryption = "start-tls"
imap.encryption = "tls" # or true backend.encryption = "tls" # or true
# IMAP server login. # IMAP server login.
# #
imap.login = "example@localhost" backend.login = "example@localhost"
# IMAP server password authentication configuration. # IMAP server password authentication configuration.
# #
imap.auth.type = "password" backend.auth.type = "password"
# #
# Password can be inlined (not recommended). # Password can be inlined (not recommended).
# #
#imap.auth.raw = "p@assw0rd" #backend.auth.raw = "p@assw0rd"
# #
# Password can be stored inside your system global keyring (requires # Password can be stored inside your system global keyring (requires
# the keyring cargo feature). You must run at least once `himalaya # the keyring cargo feature). You must run at least once `himalaya
# account configure` to set up the password. # account configure` to set up the password.
# #
#imap.auth.keyring = "example-imap" #backend.auth.keyring = "example-imap"
# #
# Password can be retrieved from a shell command. # Password can be retrieved from a shell command.
# #
imap.auth.cmd = "pass show example-imap" backend.auth.cmd = "pass show example-imap"
# IMAP server OAuth 2.0 authorization configuration. # IMAP server OAuth 2.0 authorization configuration.
# #
#imap.auth.type = "oauth2" #backend.auth.type = "oauth2"
# #
# Client identifier issued to the client during the registration # Client identifier issued to the client during the registration
# process described in RFC6749. # process described in RFC6749.
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>. # See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
# #
#imap.auth.client-id = "client-id" #backend.auth.client-id = "client-id"
# #
# Client password issued to the client during the registration process # Client password issued to the client during the registration process
# described in RFC6749. # described in RFC6749.
@ -357,23 +424,23 @@ imap.auth.cmd = "pass show example-imap"
# Defaults to keyring "<account-name>-imap-client-secret". # Defaults to keyring "<account-name>-imap-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>. # See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
# #
#imap.auth.client-secret.raw = "<raw-client-secret>" #backend.auth.client-secret.raw = "<raw-client-secret>"
#imap.auth.client-secret.keyring = "example-imap-client-secret" #backend.auth.client-secret.keyring = "example-imap-client-secret"
#imap.auth.client-secret.cmd = "pass show example-imap-client-secret" #backend.auth.client-secret.cmd = "pass show example-imap-client-secret"
# #
# Method for presenting an OAuth 2.0 bearer token to a service for # Method for presenting an OAuth 2.0 bearer token to a service for
# authentication. # authentication.
# #
#imap.auth.method = "oauthbearer" #backend.auth.method = "oauthbearer"
#imap.auth.method = "xoauth2" #backend.auth.method = "xoauth2"
# #
# URL of the authorization server's authorization endpoint. # URL of the authorization server's authorization endpoint.
# #
#imap.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth" #backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
# #
# URL of the authorization server's token endpoint. # URL of the authorization server's token endpoint.
# #
#imap.auth.token-url = "https://www.googleapis.com/oauth2/v3/token" #backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
# #
# Access token returned by the token endpoint and used to access # Access token returned by the token endpoint and used to access
# protected resources. It is recommended to use the keyring variant, # protected resources. It is recommended to use the keyring variant,
@ -381,9 +448,9 @@ imap.auth.cmd = "pass show example-imap"
# #
# Defaults to keyring "<account-name>-imap-access-token". # Defaults to keyring "<account-name>-imap-access-token".
# #
#imap.auth.access-token.raw = "<raw-access-token>" #backend.auth.access-token.raw = "<raw-access-token>"
#imap.auth.access-token.keyring = "example-imap-access-token" #backend.auth.access-token.keyring = "example-imap-access-token"
#imap.auth.access-token.cmd = "pass show example-imap-access-token" #backend.auth.access-token.cmd = "pass show example-imap-access-token"
# #
# Refresh token used to obtain a new access token (if supported by the # Refresh token used to obtain a new access token (if supported by the
# authorization server). It is recommended to use the keyring variant, # authorization server). It is recommended to use the keyring variant,
@ -391,30 +458,35 @@ imap.auth.cmd = "pass show example-imap"
# #
# Defaults to keyring "<account-name>-imap-refresh-token". # Defaults to keyring "<account-name>-imap-refresh-token".
# #
#imap.auth.refresh-token.raw = "<raw-refresh-token>" #backend.auth.refresh-token.raw = "<raw-refresh-token>"
#imap.auth.refresh-token.keyring = "example-imap-refresh-token" #backend.auth.refresh-token.keyring = "example-imap-refresh-token"
#imap.auth.refresh-token.cmd = "pass show example-imap-refresh-token" #backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
# #
# Enable the protection, as defined in RFC7636. # Enable the protection, as defined in RFC7636.
# #
# See <https://datatracker.ietf.org/doc/html/rfc7636>. # See <https://datatracker.ietf.org/doc/html/rfc7636>.
# #
#imap.auth.pkce = true #backend.auth.pkce = true
# #
# Access token scope(s), as defined by the authorization server. # Access token scope(s), as defined by the authorization server.
# #
#imap.auth.scope = "unique scope" #backend.auth.scope = "unique scope"
#imap.auth.scopes = ["multiple", "scopes"] #backend.auth.scopes = ["multiple", "scopes"]
#
# URL scheme of the redirect server.
# Defaults to http.
#
#backend.auth.redirect-scheme = "http"
# #
# Host name of the redirect server. # Host name of the redirect server.
# Defaults to localhost. # Defaults to localhost.
# #
#imap.auth.redirect-host = "localhost" #backend.auth.redirect-host = "localhost"
# #
# Port of the redirect server. # Port of the redirect server.
# Defaults to the first available one. # Defaults to the first available one.
# #
#imap.auth.redirect-port = 9999 #backend.auth.redirect-port = 9999
######################################## ########################################
#### Maildir configuration ############# #### Maildir configuration #############
@ -422,18 +494,18 @@ imap.auth.cmd = "pass show example-imap"
# Defines the Maildir backend as the default one for all features. # Defines the Maildir backend as the default one for all features.
# #
#backend = "maildir" #backend.type = "maildir"
# The Maildir root directory. The path should point to the root level # The Maildir root directory. The path should point to the root level
# of the Maildir directory. # of the Maildir directory.
# #
#maildir.root-dir = "~/.Mail/example" #backend.root-dir = "~/.Mail/example"
# Does the Maildir folder follows the Maildir++ standard? # Does the Maildir folder follows the Maildir++ standard?
# #
# See <https://en.wikipedia.org/wiki/Maildir#Maildir++>. # See <https://en.wikipedia.org/wiki/Maildir#Maildir++>.
# #
#maildir.maildirpp = false #backend.maildirpp = false
######################################## ########################################
#### Notmuch configuration ############# #### Notmuch configuration #############
@ -441,25 +513,25 @@ imap.auth.cmd = "pass show example-imap"
# Defines the Notmuch backend as the default one for all features. # Defines the Notmuch backend as the default one for all features.
# #
#backend = "notmuch" #backend.type = "notmuch"
# The path to the Notmuch database. The path should point to the root # The path to the Notmuch database. The path should point to the root
# directory containing the Notmuch database (usually the root Maildir # directory containing the Notmuch database (usually the root Maildir
# directory). # directory).
# #
#notmuch.db-path = "~/.Mail/example" #backend.db-path = "~/.Mail/example"
# Overrides the default path to the Maildir folder. # Overrides the default path to the Maildir folder.
# #
#notmuch.maildir-path = "~/.Mail/example" #backend.maildir-path = "~/.Mail/example"
# Overrides the default Notmuch configuration file path. # Overrides the default Notmuch configuration file path.
# #
#notmuch.config-path = "~/.notmuchrc" #backend.config-path = "~/.notmuchrc"
# Override the default Notmuch profile name. # Override the default Notmuch profile name.
# #
#notmuch.profile = "example" #backend.profile = "example"
######################################## ########################################
#### SMTP configuration ################ #### SMTP configuration ################
@ -467,55 +539,55 @@ imap.auth.cmd = "pass show example-imap"
# Defines the SMTP backend for the message sending feature. # Defines the SMTP backend for the message sending feature.
# #
message.send.backend = "smtp" message.send.backend.type = "smtp"
# SMTP server host name. # SMTP server host name.
# #
smtp.host = "localhost" message.send.backend.host = "localhost"
# SMTP server port. # SMTP server port.
# #
#smtp.port = 25 #message.send.backend.port = 25
#smtp.port = 465 #message.send.backend.port = 465
smtp.port = 587 message.send.backend.port = 587
# SMTP server encryption. # SMTP server encryption.
# #
#smtp.encryption = "none" # or false #message.send.backend.encryption = "none" # or false
#smtp.encryption = "start-tls" #message.send.backend.encryption = "start-tls"
smtp.encryption = "tls" # or true message.send.backend.encryption = "tls" # or true
# SMTP server login. # SMTP server login.
# #
smtp.login = "example@localhost" message.send.backend.login = "example@localhost"
# SMTP server password authentication configuration. # SMTP server password authentication configuration.
# #
smtp.auth.type = "password" message.send.backend.auth.type = "password"
# #
# Password can be inlined (not recommended). # Password can be inlined (not recommended).
# #
#smtp.auth.raw = "p@assw0rd" #message.send.backend.auth.raw = "p@assw0rd"
# #
# Password can be stored inside your system global keyring (requires # Password can be stored inside your system global keyring (requires
# the keyring cargo feature). You must run at least once `himalaya # the keyring cargo feature). You must run at least once `himalaya
# account configure` to set up the password. # account configure` to set up the password.
# #
#smtp.auth.keyring = "example-smtp" #message.send.backend.auth.keyring = "example-smtp"
# #
# Password can be retrieved from a shell command. # Password can be retrieved from a shell command.
# #
smtp.auth.cmd = "pass show example-smtp" message.send.backend.auth.cmd = "pass show example-smtp"
# SMTP server OAuth 2.0 authorization configuration. # SMTP server OAuth 2.0 authorization configuration.
# #
#smtp.auth.type = "oauth2" #message.send.backend.auth.type = "oauth2"
# #
# Client identifier issued to the client during the registration # Client identifier issued to the client during the registration
# process described in RFC6749. # process described in RFC6749.
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>. # See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
# #
#smtp.auth.client-id = "client-id" #message.send.backend.auth.client-id = "client-id"
# #
# Client password issued to the client during the registration process # Client password issued to the client during the registration process
# described in RFC6749. # described in RFC6749.
@ -523,23 +595,23 @@ smtp.auth.cmd = "pass show example-smtp"
# Defaults to keyring "<account-name>-smtp-client-secret". # Defaults to keyring "<account-name>-smtp-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>. # See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
# #
#smtp.auth.client-secret.raw = "<raw-client-secret>" #message.send.backend.auth.client-secret.raw = "<raw-client-secret>"
#smtp.auth.client-secret.keyring = "example-smtp-client-secret" #message.send.backend.auth.client-secret.keyring = "example-smtp-client-secret"
#smtp.auth.client-secret.cmd = "pass show example-smtp-client-secret" #message.send.backend.auth.client-secret.cmd = "pass show example-smtp-client-secret"
# #
# Method for presenting an OAuth 2.0 bearer token to a service for # Method for presenting an OAuth 2.0 bearer token to a service for
# authentication. # authentication.
# #
#smtp.auth.method = "oauthbearer" #message.send.backend.auth.method = "oauthbearer"
#smtp.auth.method = "xoauth2" #message.send.backend.auth.method = "xoauth2"
# #
# URL of the authorization server's authorization endpoint. # URL of the authorization server's authorization endpoint.
# #
#smtp.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth" #message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
# #
# URL of the authorization server's token endpoint. # URL of the authorization server's token endpoint.
# #
#smtp.auth.token-url = "https://www.googleapis.com/oauth2/v3/token" #message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
# #
# Access token returned by the token endpoint and used to access # Access token returned by the token endpoint and used to access
# protected resources. It is recommended to use the keyring variant, # protected resources. It is recommended to use the keyring variant,
@ -547,9 +619,9 @@ smtp.auth.cmd = "pass show example-smtp"
# #
# Defaults to keyring "<account-name>-smtp-access-token". # Defaults to keyring "<account-name>-smtp-access-token".
# #
#smtp.auth.access-token.raw = "<raw-access-token>" #message.send.backend.auth.access-token.raw = "<raw-access-token>"
#smtp.auth.access-token.keyring = "example-smtp-access-token" #message.send.backend.auth.access-token.keyring = "example-smtp-access-token"
#smtp.auth.access-token.cmd = "pass show example-smtp-access-token" #message.send.backend.auth.access-token.cmd = "pass show example-smtp-access-token"
# #
# Refresh token used to obtain a new access token (if supported by the # Refresh token used to obtain a new access token (if supported by the
# authorization server). It is recommended to use the keyring variant, # authorization server). It is recommended to use the keyring variant,
@ -557,30 +629,35 @@ smtp.auth.cmd = "pass show example-smtp"
# #
# Defaults to keyring "<account-name>-smtp-refresh-token". # Defaults to keyring "<account-name>-smtp-refresh-token".
# #
#smtp.auth.refresh-token.raw = "<raw-refresh-token>" #message.send.backend.auth.refresh-token.raw = "<raw-refresh-token>"
#smtp.auth.refresh-token.keyring = "example-smtp-refresh-token" #message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token"
#smtp.auth.refresh-token.cmd = "pass show example-smtp-refresh-token" #message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token"
# #
# Enable the protection, as defined in RFC7636. # Enable the protection, as defined in RFC7636.
# #
# See <https://datatracker.ietf.org/doc/html/rfc7636>. # See <https://datatracker.ietf.org/doc/html/rfc7636>.
# #
#smtp.auth.pkce = true #message.send.backend.auth.pkce = true
# #
# Access token scope(s), as defined by the authorization server. # Access token scope(s), as defined by the authorization server.
# #
#smtp.auth.scope = "unique scope" #message.send.backend.auth.scope = "unique scope"
#smtp.auth.scopes = ["multiple", "scopes"] #message.send.backend.auth.scopes = ["multiple", "scopes"]
#
# URL scheme of the redirect server.
# Defaults to http.
#
#message.send.backend.auth.redirect-scheme = "http"
# #
# Host name of the redirect server. # Host name of the redirect server.
# Defaults to localhost. # Defaults to localhost.
# #
#smtp.auth.redirect-host = "localhost" #message.send.backend.auth.redirect-host = "localhost"
# #
# Port of the redirect server. # Port of the redirect server.
# Defaults to the first available one. # Defaults to the first available one.
# #
#smtp.auth.redirect-port = 9999 #message.send.backend.auth.redirect-port = 9999
######################################## ########################################
#### Sendmail configuration ############ #### Sendmail configuration ############
@ -588,8 +665,8 @@ smtp.auth.cmd = "pass show example-smtp"
# Defines the Sendmail backend for the message sending feature. # Defines the Sendmail backend for the message sending feature.
# #
#message.send.backend = "sendmail" #message.send.backend.type = "sendmail"
# Customizes the sendmail shell command. # Customizes the sendmail shell command.
# #
#sendmail.cmd = "/usr/bin/sendmail" #message.send.backend.cmd = "/usr/bin/sendmail"

View file

@ -1,11 +1,25 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::context::BackendContextBuilder; use email::backend::BackendBuilder;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
#[cfg(feature = "maildir")]
use email::maildir::MaildirContextBuilder;
#[cfg(feature = "notmuch")]
use email::notmuch::NotmuchContextBuilder;
#[cfg(feature = "sendmail")]
use email::sendmail::SendmailContextBuilder;
#[cfg(feature = "smtp")]
use email::smtp::SmtpContextBuilder;
use pimalaya_tui::{
himalaya::config::{Backend, SendingBackend},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
account::arg::name::OptionalAccountNameArg, backend, config::Config, printer::Printer,
};
/// Check up the given account. /// Check up the given account.
/// ///
@ -19,102 +33,77 @@ pub struct AccountCheckUpCommand {
} }
impl AccountCheckUpCommand { impl AccountCheckUpCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing check up account command"); info!("executing check up account command");
let account = self.account.name.as_ref().map(String::as_str); let account = self.account.name.as_ref().map(String::as_str);
printer.log("Checking configuration integrity…")?; printer.log("Checking configuration integrity…\n")?;
let (toml_account_config, account_config) = config.clone().into_account_configs(account)?; let (toml_account_config, account_config) = config.clone().into_account_configs(account)?;
let used_backends = toml_account_config.get_used_backends(); let account_config = Arc::new(account_config);
printer.log("Checking backend context integrity…")?;
let ctx_builder = backend::BackendContextBuilder::new(
toml_account_config.clone(),
account_config,
Vec::from_iter(used_backends),
)
.await?;
let ctx = ctx_builder.clone().build().await?;
match toml_account_config.backend {
#[cfg(feature = "maildir")] #[cfg(feature = "maildir")]
{ Some(Backend::Maildir(mdir_config)) => {
printer.log("Checking Maildir integrity…")?; printer.log("Checking Maildir integrity…\n")?;
let maildir = ctx_builder let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
.maildir BackendBuilder::new(account_config.clone(), ctx)
.as_ref() .check_up()
.and_then(|maildir| maildir.check_up()) .await?;
.and_then(|f| ctx.maildir.as_ref().and_then(|ctx| f(ctx)));
if let Some(maildir) = maildir.as_ref() {
maildir.check_up().await?;
} }
}
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
{ Some(Backend::Imap(imap_config)) => {
printer.log("Checking IMAP integrity…")?; printer.log("Checking IMAP integrity…\n")?;
let imap = ctx_builder let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
.imap .with_pool_size(1);
.as_ref() BackendBuilder::new(account_config.clone(), ctx)
.and_then(|imap| imap.check_up()) .check_up()
.and_then(|f| ctx.imap.as_ref().and_then(|ctx| f(ctx))); .await?;
if let Some(imap) = imap.as_ref() {
imap.check_up().await?;
} }
}
#[cfg(feature = "notmuch")] #[cfg(feature = "notmuch")]
{ Some(Backend::Notmuch(notmuch_config)) => {
printer.log("Checking Notmuch integrity…")?; printer.log("Checking Notmuch integrity…\n")?;
let notmuch = ctx_builder let ctx =
.notmuch NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
.as_ref() BackendBuilder::new(account_config.clone(), ctx)
.and_then(|notmuch| notmuch.check_up()) .check_up()
.and_then(|f| ctx.notmuch.as_ref().and_then(|ctx| f(ctx))); .await?;
if let Some(notmuch) = notmuch.as_ref() {
notmuch.check_up().await?;
} }
_ => (),
} }
let sending_backend = toml_account_config
.message
.and_then(|msg| msg.send)
.and_then(|send| send.backend);
match sending_backend {
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
{ Some(SendingBackend::Smtp(smtp_config)) => {
printer.log("Checking SMTP integrity…")?; printer.log("Checking SMTP integrity…\n")?;
let smtp = ctx_builder let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
.smtp BackendBuilder::new(account_config.clone(), ctx)
.as_ref() .check_up()
.and_then(|smtp| smtp.check_up()) .await?;
.and_then(|f| ctx.smtp.as_ref().and_then(|ctx| f(ctx)));
if let Some(smtp) = smtp.as_ref() {
smtp.check_up().await?;
} }
}
#[cfg(feature = "sendmail")] #[cfg(feature = "sendmail")]
{ Some(SendingBackend::Sendmail(sendmail_config)) => {
printer.log("Checking Sendmail integrity…")?; printer.log("Checking Sendmail integrity…\n")?;
let sendmail = ctx_builder let ctx =
.sendmail SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
.as_ref() BackendBuilder::new(account_config.clone(), ctx)
.and_then(|sendmail| sendmail.check_up()) .check_up()
.and_then(|f| ctx.sendmail.as_ref().and_then(|ctx| f(ctx))); .await?;
if let Some(sendmail) = sendmail.as_ref() {
sendmail.check_up().await?;
} }
_ => (),
} }
printer.out("Checkup successfully completed!") printer.out("Checkup successfully completed!\n")
} }
} }

View file

@ -5,12 +5,13 @@ use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig; use email::smtp::config::SmtpAuthConfig;
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))] #[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
use pimalaya_tui::prompt; use pimalaya_tui::terminal::prompt;
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info; use tracing::info;
#[cfg(any(feature = "imap", feature = "smtp"))] #[cfg(any(feature = "imap", feature = "smtp"))]
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::{account::arg::name::AccountNameArg, config::Config, printer::Printer}; use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
/// Configure an account. /// Configure an account.
/// ///
@ -31,20 +32,22 @@ pub struct AccountConfigureCommand {
} }
impl AccountConfigureCommand { impl AccountConfigureCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing configure account command"); info!("executing configure account command");
let account = &self.account.name; let account = &self.account.name;
let (_, account_config) = config.into_toml_account_config(Some(account))?; let (_, toml_account_config) = config.to_toml_account_config(Some(account))?;
if self.reset { if self.reset {
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap { {
let reset = match &config.auth { let reset = match toml_account_config.imap_auth_config() {
ImapAuthConfig::Passwd(config) => config.reset().await, Some(ImapAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")] #[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => config.reset().await, Some(ImapAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
}; };
if let Err(err) = reset { if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}"); warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}"); debug!("error while resetting imap secrets: {err:?}");
@ -52,12 +55,14 @@ impl AccountConfigureCommand {
} }
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp { {
let reset = match &config.auth { let reset = match toml_account_config.smtp_auth_config() {
SmtpAuthConfig::Passwd(config) => config.reset().await, Some(SmtpAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")] #[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => config.reset().await, Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
}; };
if let Err(err) = reset { if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}"); warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}"); debug!("error while resetting smtp secrets: {err:?}");
@ -65,56 +70,54 @@ impl AccountConfigureCommand {
} }
#[cfg(feature = "pgp")] #[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp { if let Some(config) = &toml_account_config.pgp {
config.reset().await?; config.reset().await?;
} }
} }
#[cfg(feature = "imap")] #[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap { match toml_account_config.imap_auth_config() {
match &config.auth { Some(ImapAuthConfig::Password(config)) => {
ImapAuthConfig::Passwd(config) => {
config config
.configure(|| Ok(prompt::password("IMAP password")?)) .configure(|| Ok(prompt::password("IMAP password")?))
.await .await
} }
#[cfg(feature = "oauth2")] #[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => { Some(ImapAuthConfig::OAuth2(config)) => {
config config
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?)) .configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
.await .await
} }
_ => Ok(()),
}?; }?;
}
#[cfg(feature = "smtp")] #[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp { match toml_account_config.smtp_auth_config() {
match &config.auth { Some(SmtpAuthConfig::Password(config)) => {
SmtpAuthConfig::Passwd(config) => {
config config
.configure(|| Ok(prompt::password("SMTP password")?)) .configure(|| Ok(prompt::password("SMTP password")?))
.await .await
} }
#[cfg(feature = "oauth2")] #[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => { Some(SmtpAuthConfig::OAuth2(config)) => {
config config
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?)) .configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await .await
} }
_ => Ok(()),
}?; }?;
}
#[cfg(feature = "pgp")] #[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp { if let Some(config) = &toml_account_config.pgp {
config config
.configure(&account_config.email, || { .configure(&toml_account_config.email, || {
Ok(prompt::password("PGP secret key password")?) Ok(prompt::password("PGP secret key password")?)
}) })
.await?; .await?;
} }
printer.out(format!( printer.out(format!(
"Account {account} successfully {}configured!", "Account {account} successfully {}configured!\n",
if self.reset { "re" } else { "" } if self.reset { "re" } else { "" }
)) ))
} }

View file

@ -1,12 +1,12 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::{
himalaya::config::{Accounts, AccountsTable},
terminal::cli::printer::Printer,
};
use tracing::info; use tracing::info;
use crate::{ use crate::config::TomlConfig;
account::{Accounts, AccountsTable},
config::Config,
printer::Printer,
};
/// List all accounts. /// List all accounts.
/// ///
@ -24,7 +24,7 @@ pub struct AccountListCommand {
} }
impl AccountListCommand { impl AccountListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list accounts command"); info!("executing list accounts command");
let accounts = Accounts::from(config.accounts.iter()); let accounts = Accounts::from(config.accounts.iter());

View file

@ -4,8 +4,9 @@ mod list;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{ use self::{
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand, check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
@ -30,7 +31,7 @@ pub enum AccountSubcommand {
impl AccountSubcommand { impl AccountSubcommand {
#[allow(unused)] #[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::CheckUp(cmd) => cmd.execute(printer, config).await, Self::CheckUp(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(printer, config).await, Self::Configure(cmd) => cmd.execute(printer, config).await,

View file

@ -1,363 +1,3 @@
//! Deserialized account config module. use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
use comfy_table::presets; pub type TomlAccountConfig = HimalayaTomlAccountConfig;
use crossterm::style::Color;
#[cfg(feature = "pgp")]
use email::account::config::pgp::PgpConfig;
#[cfg(feature = "imap")]
use email::imap::config::ImapConfig;
#[cfg(feature = "maildir")]
use email::maildir::config::MaildirConfig;
#[cfg(feature = "notmuch")]
use email::notmuch::config::NotmuchConfig;
#[cfg(feature = "sendmail")]
use email::sendmail::config::SendmailConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpConfig;
use email::template::config::TemplateConfig;
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, ui::map_color,
};
/// Represents all existing kind of account config.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
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 backend: Option<BackendKind>,
#[cfg(feature = "pgp")]
pub pgp: Option<PgpConfig>,
pub folder: Option<FolderConfig>,
pub envelope: Option<EnvelopeConfig>,
pub flag: Option<FlagConfig>,
pub message: Option<MessageConfig>,
pub template: Option<TemplateConfig>,
#[cfg(feature = "imap")]
pub imap: Option<ImapConfig>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirConfig>,
#[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 folder_list_table_preset(&self) -> Option<String> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn folder_list_table_name_color(&self) -> Option<Color> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.name_color)
}
pub fn folder_list_table_desc_color(&self) -> Option<Color> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.desc_color)
}
pub fn envelope_list_table_preset(&self) -> Option<String> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn envelope_list_table_unseen_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.unseen_char)
}
pub fn envelope_list_table_replied_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.replied_char)
}
pub fn envelope_list_table_flagged_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.flagged_char)
}
pub fn envelope_list_table_attachment_char(&self) -> Option<char> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.attachment_char)
}
pub fn envelope_list_table_id_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.id_color)
}
pub fn envelope_list_table_flags_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.flags_color)
}
pub fn envelope_list_table_subject_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.subject_color)
}
pub fn envelope_list_table_sender_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.sender_color)
}
pub fn envelope_list_table_date_color(&self) -> Option<Color> {
self.envelope
.as_ref()
.and_then(|env| env.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.date_color)
}
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(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(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(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(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(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(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(self.backend.as_ref())
}
pub fn thread_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.thread.as_ref())
.and_then(|thread| thread.backend.as_ref())
.or(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(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(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(self.backend.as_ref())
}
pub fn add_message_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|msg| msg.write.as_ref())
.and_then(|add| add.backend.as_ref())
.or(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(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(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(self.backend.as_ref())
}
pub fn move_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.r#move.as_ref())
.and_then(|move_| move_.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn delete_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.delete.as_ref())
.and_then(|delete| delete.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn send_message_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|msg| msg.send.as_ref())
.and_then(|send| send.backend.as_ref())
.or(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
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListAccountsTableConfig {
pub preset: Option<String>,
pub name_color: Option<Color>,
pub backends_color: Option<Color>,
pub default_color: Option<Color>,
}
impl ListAccountsTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn name_color(&self) -> comfy_table::Color {
map_color(self.name_color.unwrap_or(Color::Green))
}
pub fn backends_color(&self) -> comfy_table::Color {
map_color(self.backends_color.unwrap_or(Color::Blue))
}
pub fn default_color(&self) -> comfy_table::Color {
map_color(self.default_color.unwrap_or(Color::Reset))
}
}

View file

@ -1,198 +1,3 @@
pub mod arg; pub mod arg;
pub mod command; pub mod command;
pub mod config; pub mod config;
#[cfg(feature = "wizard")]
pub(crate) mod wizard;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use crossterm::style::Color;
use serde::{Serialize, Serializer};
use std::{collections::hash_map::Iter, fmt, ops::Deref};
use self::config::{ListAccountsTableConfig, 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,
}
}
pub fn to_row(&self, config: &ListAccountsTableConfig) -> Row {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&self.name).fg(config.name_color()));
row.add_cell(Cell::new(&self.backend).fg(config.backends_color()));
row.add_cell(Cell::new(if self.default { "yes" } else { "" }).fg(config.default_color()));
row
}
}
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
/// Represents the list of printable accounts.
#[derive(Debug, Default, Serialize)]
pub struct Accounts(Vec<Account>);
impl Deref for Accounts {
type Target = Vec<Account>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Iter<'_, String, TomlAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, account)| {
#[allow(unused_mut)]
let mut backends = String::new();
#[cfg(feature = "imap")]
if account.imap.is_some() {
backends.push_str("imap");
}
#[cfg(feature = "maildir")]
if account.maildir.is_some() {
if !backends.is_empty() {
backends.push_str(", ")
}
backends.push_str("maildir");
}
#[cfg(feature = "notmuch")]
if account.notmuch.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");
}
#[cfg(feature = "sendmail")]
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)
}
}
pub struct AccountsTable {
accounts: Accounts,
width: Option<u16>,
config: ListAccountsTableConfig,
}
impl AccountsTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_name_color(mut self, color: Option<Color>) -> Self {
self.config.name_color = color;
self
}
pub fn with_some_backends_color(mut self, color: Option<Color>) -> Self {
self.config.backends_color = color;
self
}
pub fn with_some_default_color(mut self, color: Option<Color>) -> Self {
self.config.default_color = color;
self
}
}
impl From<Accounts> for AccountsTable {
fn from(accounts: Accounts) -> Self {
Self {
accounts,
width: None,
config: Default::default(),
}
}
}
impl fmt::Display for AccountsTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("NAME"),
Cell::new("BACKENDS"),
Cell::new("DEFAULT"),
]))
.add_rows(
self.accounts
.iter()
.map(|account| account.to_row(&self.config)),
);
if let Some(width) = self.width {
table.set_width(width);
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for AccountsTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.accounts.serialize(serializer)
}
}

View file

@ -1,95 +0,0 @@
use color_eyre::Result;
use pimalaya_tui::{print, prompt};
use crate::{
backend::{self, config::BackendConfig, BackendKind},
message::config::{MessageConfig, MessageSendConfig},
};
use super::TomlAccountConfig;
pub async fn configure() -> Result<(String, TomlAccountConfig)> {
let email = prompt::email("Email address:", None)?;
let mut config = TomlAccountConfig {
email: email.to_string(),
..Default::default()
};
let autoconfig_email = config.email.to_owned();
let autoconfig =
tokio::spawn(async move { email::autoconfig::from_addr(&autoconfig_email).await.ok() });
let default_account_name = email
.domain()
.split_once('.')
.map(|domain| domain.0)
.unwrap_or(email.domain());
let account_name = prompt::text("Account name:", Some(default_account_name))?;
config.display_name = Some(prompt::text(
"Full display name:",
Some(email.local_part()),
)?);
config.downloads_dir = Some(prompt::path("Downloads directory:", Some("~/Downloads"))?);
let autoconfig = autoconfig.await?;
let autoconfig = autoconfig.as_ref();
if let Some(config) = autoconfig {
if config.is_gmail() {
println!();
print::warn("Warning: Google passwords cannot be used directly, see:");
print::warn("https://github.com/pimalaya/himalaya?tab=readme-ov-file#configuration");
println!();
}
}
match backend::wizard::configure(&account_name, &email, autoconfig).await? {
#[cfg(feature = "imap")]
BackendConfig::Imap(imap_config) => {
config.imap = Some(imap_config);
config.backend = Some(BackendKind::Imap);
}
#[cfg(feature = "maildir")]
BackendConfig::Maildir(mdir_config) => {
config.maildir = Some(mdir_config);
config.backend = Some(BackendKind::Maildir);
}
#[cfg(feature = "notmuch")]
BackendConfig::Notmuch(notmuch_config) => {
config.notmuch = Some(notmuch_config);
config.backend = Some(BackendKind::Notmuch);
}
_ => unreachable!(),
};
match backend::wizard::configure_sender(&account_name, &email, autoconfig).await? {
#[cfg(feature = "smtp")]
BackendConfig::Smtp(smtp_config) => {
config.smtp = Some(smtp_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Smtp),
..Default::default()
}),
..Default::default()
});
}
#[cfg(feature = "sendmail")]
BackendConfig::Sendmail(sendmail_config) => {
config.sendmail = Some(sendmail_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Sendmail),
..Default::default()
}),
..Default::default()
});
}
_ => unreachable!(),
};
Ok((account_name, config))
}

View file

@ -1,24 +0,0 @@
#[cfg(feature = "imap")]
use email::imap::config::ImapConfig;
#[cfg(feature = "maildir")]
use email::maildir::config::MaildirConfig;
#[cfg(feature = "notmuch")]
use email::notmuch::config::NotmuchConfig;
#[cfg(feature = "sendmail")]
use email::sendmail::config::SendmailConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpConfig;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendConfig {
#[cfg(feature = "imap")]
Imap(ImapConfig),
#[cfg(feature = "maildir")]
Maildir(MaildirConfig),
#[cfg(feature = "notmuch")]
Notmuch(NotmuchConfig),
#[cfg(feature = "smtp")]
Smtp(SmtpConfig),
#[cfg(feature = "sendmail")]
Sendmail(SendmailConfig),
}

View file

@ -1,735 +0,0 @@
pub mod config;
#[cfg(feature = "wizard")]
pub(crate) mod wizard;
use async_trait::async_trait;
use color_eyre::Result;
use std::{fmt::Display, ops::Deref, sync::Arc};
use tracing::instrument;
#[cfg(feature = "imap")]
use email::imap::{ImapContext, ImapContextBuilder};
#[cfg(any(feature = "account-sync", feature = "maildir"))]
use email::maildir::{MaildirContextBuilder, MaildirContextSync};
#[cfg(feature = "notmuch")]
use email::notmuch::{NotmuchContextBuilder, NotmuchContextSync};
#[cfg(feature = "sendmail")]
use email::sendmail::{SendmailContextBuilder, SendmailContextSync};
#[cfg(feature = "smtp")]
use email::smtp::{SmtpContextBuilder, SmtpContextSync};
use email::{
account::config::AccountConfig,
backend::{
feature::BackendFeature, macros::BackendContext, mapper::SomeBackendContextBuilderMapper,
},
envelope::{
get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions},
thread::ThreadEnvelopes,
Id, SingleId,
},
flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags},
folder::{
add::AddFolder, delete::DeleteFolder, expunge::ExpungeFolder, list::ListFolders,
purge::PurgeFolder,
},
message::{
add::AddMessage,
copy::CopyMessages,
delete::DeleteMessages,
get::GetMessages,
peek::PeekMessages,
r#move::MoveMessages,
send::{SendMessage, SendMessageThenSaveCopy},
Messages,
},
AnyResult,
};
use serde::{Deserialize, Serialize};
use crate::{
account::config::TomlAccountConfig,
cache::IdMapper,
envelope::{Envelopes, ThreadedEnvelopes},
};
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
None,
#[cfg(feature = "imap")]
Imap,
#[cfg(feature = "maildir")]
Maildir,
#[cfg(feature = "notmuch")]
Notmuch,
#[cfg(feature = "smtp")]
Smtp,
#[cfg(feature = "sendmail")]
Sendmail,
}
impl Display for BackendKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::None => "None",
#[cfg(feature = "imap")]
Self::Imap => "IMAP",
#[cfg(feature = "maildir")]
Self::Maildir => "Maildir",
#[cfg(feature = "notmuch")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp")]
Self::Smtp => "SMTP",
#[cfg(feature = "sendmail")]
Self::Sendmail => "Sendmail",
}
)
}
}
#[derive(Clone, Default)]
pub struct BackendContextBuilder {
pub toml_account_config: Arc<TomlAccountConfig>,
pub account_config: Arc<AccountConfig>,
#[cfg(feature = "imap")]
pub imap: Option<ImapContextBuilder>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextBuilder>,
#[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchContextBuilder>,
#[cfg(feature = "smtp")]
pub smtp: Option<SmtpContextBuilder>,
#[cfg(feature = "sendmail")]
pub sendmail: Option<SendmailContextBuilder>,
}
impl BackendContextBuilder {
pub async fn new(
toml_account_config: Arc<TomlAccountConfig>,
account_config: Arc<AccountConfig>,
kinds: Vec<&BackendKind>,
) -> Result<Self> {
Ok(Self {
toml_account_config: toml_account_config.clone(),
account_config: account_config.clone(),
#[cfg(feature = "imap")]
imap: {
let builder = toml_account_config
.imap
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Imap))
.map(Clone::clone)
.map(Arc::new)
.map(|imap_config| {
ImapContextBuilder::new(account_config.clone(), imap_config)
.with_prebuilt_credentials()
});
match builder {
Some(builder) => Some(builder.await?),
None => None,
}
},
#[cfg(feature = "maildir")]
maildir: toml_account_config
.maildir
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Maildir))
.map(Clone::clone)
.map(Arc::new)
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
#[cfg(feature = "notmuch")]
notmuch: toml_account_config
.notmuch
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Notmuch))
.map(Clone::clone)
.map(Arc::new)
.map(|notmuch_config| {
NotmuchContextBuilder::new(account_config.clone(), notmuch_config)
}),
#[cfg(feature = "smtp")]
smtp: toml_account_config
.smtp
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Smtp))
.map(Clone::clone)
.map(Arc::new)
.map(|smtp_config| SmtpContextBuilder::new(account_config.clone(), smtp_config)),
#[cfg(feature = "sendmail")]
sendmail: toml_account_config
.sendmail
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Sendmail))
.map(Clone::clone)
.map(Arc::new)
.map(|sendmail_config| {
SendmailContextBuilder::new(account_config.clone(), sendmail_config)
}),
})
}
}
#[async_trait]
impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
type Context = BackendContext;
fn add_folder(&self) -> Option<BackendFeature<Self::Context, dyn AddFolder>> {
match self.toml_account_config.add_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_folder_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_folder_with_some(&self.notmuch),
_ => None,
}
}
fn list_folders(&self) -> Option<BackendFeature<Self::Context, dyn ListFolders>> {
match self.toml_account_config.list_folders_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_folders_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_folders_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.list_folders_with_some(&self.notmuch),
_ => None,
}
}
fn expunge_folder(&self) -> Option<BackendFeature<Self::Context, dyn ExpungeFolder>> {
match self.toml_account_config.expunge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.expunge_folder_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.expunge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.expunge_folder_with_some(&self.notmuch),
_ => None,
}
}
fn purge_folder(&self) -> Option<BackendFeature<Self::Context, dyn PurgeFolder>> {
match self.toml_account_config.purge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.purge_folder_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.purge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.purge_folder_with_some(&self.notmuch),
_ => None,
}
}
fn delete_folder(&self) -> Option<BackendFeature<Self::Context, dyn DeleteFolder>> {
match self.toml_account_config.delete_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_folder_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.delete_folder_with_some(&self.notmuch),
_ => None,
}
}
fn get_envelope(&self) -> Option<BackendFeature<Self::Context, dyn GetEnvelope>> {
match self.toml_account_config.get_envelope_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_envelope_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_envelope_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.get_envelope_with_some(&self.notmuch),
_ => None,
}
}
fn list_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ListEnvelopes>> {
match self.toml_account_config.list_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_envelopes_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.list_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn thread_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ThreadEnvelopes>> {
match self.toml_account_config.thread_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.thread_envelopes_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.thread_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.thread_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn add_flags(&self) -> Option<BackendFeature<Self::Context, dyn AddFlags>> {
match self.toml_account_config.add_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_flags_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_flags_with_some(&self.notmuch),
_ => None,
}
}
fn set_flags(&self) -> Option<BackendFeature<Self::Context, dyn SetFlags>> {
match self.toml_account_config.set_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.set_flags_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.set_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.set_flags_with_some(&self.notmuch),
_ => None,
}
}
fn remove_flags(&self) -> Option<BackendFeature<Self::Context, dyn RemoveFlags>> {
match self.toml_account_config.remove_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.remove_flags_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.remove_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.remove_flags_with_some(&self.notmuch),
_ => None,
}
}
fn add_message(&self) -> Option<BackendFeature<Self::Context, dyn AddMessage>> {
match self.toml_account_config.add_message_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_message_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_message_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_message_with_some(&self.notmuch),
_ => None,
}
}
fn send_message(&self) -> Option<BackendFeature<Self::Context, dyn SendMessage>> {
match self.toml_account_config.send_message_kind() {
#[cfg(feature = "smtp")]
Some(BackendKind::Smtp) => self.send_message_with_some(&self.smtp),
#[cfg(feature = "sendmail")]
Some(BackendKind::Sendmail) => self.send_message_with_some(&self.sendmail),
_ => None,
}
}
fn peek_messages(&self) -> Option<BackendFeature<Self::Context, dyn PeekMessages>> {
match self.toml_account_config.peek_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.peek_messages_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.peek_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.peek_messages_with_some(&self.notmuch),
_ => None,
}
}
fn get_messages(&self) -> Option<BackendFeature<Self::Context, dyn GetMessages>> {
match self.toml_account_config.get_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_messages_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.get_messages_with_some(&self.notmuch),
_ => None,
}
}
fn copy_messages(&self) -> Option<BackendFeature<Self::Context, dyn CopyMessages>> {
match self.toml_account_config.copy_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.copy_messages_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.copy_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.copy_messages_with_some(&self.notmuch),
_ => None,
}
}
fn move_messages(&self) -> Option<BackendFeature<Self::Context, dyn MoveMessages>> {
match self.toml_account_config.move_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.move_messages_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.move_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.move_messages_with_some(&self.notmuch),
_ => None,
}
}
fn delete_messages(&self) -> Option<BackendFeature<Self::Context, dyn DeleteMessages>> {
match self.toml_account_config.delete_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_messages_with_some(&self.imap),
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.delete_messages_with_some(&self.notmuch),
_ => None,
}
}
async fn build(self) -> AnyResult<Self::Context> {
let mut ctx = BackendContext::default();
#[cfg(feature = "imap")]
if let Some(imap) = self.imap {
ctx.imap = Some(imap.build().await?);
}
#[cfg(feature = "maildir")]
if let Some(maildir) = self.maildir {
ctx.maildir = Some(maildir.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?);
}
#[cfg(feature = "sendmail")]
if let Some(sendmail) = self.sendmail {
ctx.sendmail = Some(sendmail.build().await?);
}
Ok(ctx)
}
}
#[derive(BackendContext, Default)]
pub struct BackendContext {
#[cfg(feature = "imap")]
pub imap: Option<ImapContext>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextSync>,
#[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchContextSync>,
#[cfg(feature = "smtp")]
pub smtp: Option<SmtpContextSync>,
#[cfg(feature = "sendmail")]
pub sendmail: Option<SendmailContextSync>,
}
#[cfg(feature = "imap")]
impl AsRef<Option<ImapContext>> for BackendContext {
fn as_ref(&self) -> &Option<ImapContext> {
&self.imap
}
}
#[cfg(feature = "maildir")]
impl AsRef<Option<MaildirContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<MaildirContextSync> {
&self.maildir
}
}
#[cfg(feature = "notmuch")]
impl AsRef<Option<NotmuchContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<NotmuchContextSync> {
&self.notmuch
}
}
#[cfg(feature = "smtp")]
impl AsRef<Option<SmtpContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<SmtpContextSync> {
&self.smtp
}
}
#[cfg(feature = "sendmail")]
impl AsRef<Option<SendmailContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<SendmailContextSync> {
&self.sendmail
}
}
pub struct Backend {
pub toml_account_config: Arc<TomlAccountConfig>,
pub backend: email::backend::Backend<BackendContext>,
}
impl Backend {
pub async fn new(
toml_account_config: Arc<TomlAccountConfig>,
account_config: Arc<AccountConfig>,
backend_kinds: impl IntoIterator<Item = &BackendKind>,
with_features: impl Fn(&mut email::backend::BackendBuilder<BackendContextBuilder>),
) -> Result<Self> {
let backend_kinds = backend_kinds.into_iter().collect();
let backend_ctx_builder = BackendContextBuilder::new(
toml_account_config.clone(),
account_config.clone(),
backend_kinds,
)
.await?;
let mut backend_builder =
email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder)
.without_features();
with_features(&mut backend_builder);
Ok(Self {
toml_account_config: toml_account_config.clone(),
backend: backend_builder.build().await?,
})
}
#[instrument(skip(self))]
fn build_id_mapper(
&self,
folder: &str,
backend_kind: Option<&BackendKind>,
) -> Result<IdMapper> {
#[allow(unused_mut)]
#[cfg(feature = "maildir")]
if let Some(BackendKind::Maildir) = backend_kind {
if let Some(_) = &self.toml_account_config.maildir {
return Ok(IdMapper::new(&self.backend.account_config, folder)?);
}
}
#[cfg(feature = "notmuch")]
if let Some(BackendKind::Notmuch) = backend_kind {
if let Some(_) = &self.toml_account_config.notmuch {
return Ok(IdMapper::new(&self.backend.account_config, folder)?);
}
}
Ok(IdMapper::Dummy)
}
pub async fn list_envelopes(
&self,
folder: &str,
opts: ListEnvelopesOptions,
) -> 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, opts).await?;
let envelopes =
Envelopes::try_from_backend(&self.backend.account_config, &id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn thread_envelopes(
&self,
folder: &str,
opts: ListEnvelopesOptions,
) -> Result<ThreadedEnvelopes> {
let backend_kind = self.toml_account_config.thread_envelopes_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let envelopes = self.backend.thread_envelopes(folder, opts).await?;
let envelopes = ThreadedEnvelopes::try_from_backend(&id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn thread_envelope(
&self,
folder: &str,
id: usize,
opts: ListEnvelopesOptions,
) -> Result<ThreadedEnvelopes> {
let backend_kind = self.toml_account_config.thread_envelopes_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = id_mapper.get_id(id)?;
let envelopes = self
.backend
.thread_envelope(folder, SingleId::from(id), opts)
.await?;
let envelopes = ThreadedEnvelopes::try_from_backend(&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?;
Ok(())
}
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?;
Ok(())
}
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?;
Ok(())
}
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?;
Ok(())
}
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?;
Ok(())
}
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?;
Ok(())
}
pub async fn add_message(&self, folder: &str, email: &[u8]) -> Result<SingleId> {
let backend_kind = self.toml_account_config.add_message_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = self.backend.add_message(folder, email).await?;
id_mapper.create_alias(&*id)?;
Ok(id)
}
pub async fn add_message_with_flags(
&self,
folder: &str,
email: &[u8],
flags: &Flags,
) -> Result<SingleId> {
let backend_kind = self.toml_account_config.add_message_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = self
.backend
.add_message_with_flags(folder, email, flags)
.await?;
id_mapper.create_alias(&*id)?;
Ok(id)
}
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)?);
let msgs = self.backend.peek_messages(folder, &ids).await?;
Ok(msgs)
}
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)?);
let msgs = self.backend.get_messages(folder, &ids).await?;
Ok(msgs)
}
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?;
Ok(())
}
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?;
Ok(())
}
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?;
Ok(())
}
pub async fn send_message_then_save_copy(&self, msg: &[u8]) -> Result<()> {
self.backend.send_message_then_save_copy(msg).await?;
Ok(())
}
}
impl Deref for Backend {
type Target = email::backend::Backend<BackendContext>;
fn deref(&self) -> &Self::Target {
&self.backend
}
}

View file

@ -1,75 +0,0 @@
use color_eyre::Result;
use email::autoconfig::config::AutoConfig;
use email_address::EmailAddress;
use pimalaya_tui::{prompt, wizard};
use super::{config::BackendConfig, BackendKind};
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
#[cfg(feature = "imap")]
BackendKind::Imap,
#[cfg(feature = "maildir")]
BackendKind::Maildir,
#[cfg(feature = "notmuch")]
BackendKind::Notmuch,
];
pub async fn configure(
account_name: &str,
email: &EmailAddress,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
let backend = prompt::item("Default backend:", &*DEFAULT_BACKEND_KINDS, None)?;
match backend {
#[cfg(feature = "imap")]
BackendKind::Imap => {
let config = wizard::imap::start(account_name, email, autoconfig).await?;
Ok(BackendConfig::Imap(config))
}
#[cfg(feature = "maildir")]
BackendKind::Maildir => {
let config = wizard::maildir::start(account_name)?;
Ok(BackendConfig::Maildir(config))
}
#[cfg(feature = "notmuch")]
BackendKind::Notmuch => {
let config = wizard::notmuch::start()?;
Ok(BackendConfig::Notmuch(config))
}
_ => unreachable!(),
}
}
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
#[cfg(feature = "smtp")]
BackendKind::Smtp,
#[cfg(feature = "sendmail")]
BackendKind::Sendmail,
];
pub async fn configure_sender(
account_name: &str,
email: &EmailAddress,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
let backend = prompt::item(
"Backend for sending messages:",
&*SEND_MESSAGE_BACKEND_KINDS,
None,
)?;
match backend {
#[cfg(feature = "smtp")]
BackendKind::Smtp => {
let config = wizard::smtp::start(account_name, email, autoconfig).await?;
Ok(BackendConfig::Smtp(config))
}
#[cfg(feature = "sendmail")]
BackendKind::Sendmail => {
let config = wizard::sendmail::start()?;
Ok(BackendConfig::Sendmail(config))
}
_ => unreachable!(),
}
}

View file

@ -1,15 +0,0 @@
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,
}

View file

@ -1 +0,0 @@
pub mod disable;

144
src/cache/mod.rs vendored
View file

@ -1,144 +0,0 @@
pub mod arg;
use color_eyre::{eyre::eyre, eyre::Context, Result};
use dirs::data_dir;
use email::account::config::AccountConfig;
use sled::{Config, Db};
use std::collections::HashSet;
use tracing::debug;
#[derive(Debug)]
pub enum IdMapper {
Dummy,
Mapper(Db),
}
impl IdMapper {
pub fn new(account_config: &AccountConfig, folder: &str) -> Result<Self> {
let digest = md5::compute(account_config.name.clone() + folder);
let db_path = data_dir()
.ok_or(eyre!("cannot get XDG data directory"))?
.join("himalaya")
.join(".id-mappers")
.join(format!("{digest:x}"));
let conn = Config::new()
.path(&db_path)
.idgen_persist_interval(1)
.open()
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
Ok(Self::Mapper(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(conn) => {
debug!("creating alias for id {id}…");
let alias = conn
.generate_id()
.with_context(|| format!("cannot create alias for id {id}"))?
.to_string();
debug!("created alias {alias} for id {id}");
conn.insert(&id, alias.as_bytes())
.with_context(|| format!("cannot insert 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(conn) => {
debug!("getting alias for id {id}…");
let alias = conn
.get(id)
.with_context(|| format!("cannot get alias for id {id}"))?;
let alias = match alias {
Some(alias) => {
let alias = String::from_utf8_lossy(alias.as_ref());
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();
match self {
Self::Dummy => Ok(alias.to_string()),
Self::Mapper(conn) => {
debug!("getting id from alias {alias}…");
let id = conn
.iter()
.flat_map(|entry| entry)
.find_map(|(entry_id, entry_alias)| {
if entry_alias.as_ref() == alias.as_bytes() {
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
Some(entry_id.to_string())
} else {
None
}
})
.ok_or_else(|| eyre!("cannot get id from alias {alias}"))?;
debug!("found id {id} from alias {alias}");
Ok(id)
}
}
}
pub fn get_ids(&self, aliases: impl IntoIterator<Item = impl ToString>) -> Result<Vec<String>> {
let aliases: Vec<String> = aliases.into_iter().map(|alias| alias.to_string()).collect();
match self {
Self::Dummy => Ok(aliases),
Self::Mapper(conn) => {
let aliases: HashSet<&str> = aliases.iter().map(|alias| alias.as_str()).collect();
let ids: Vec<String> = conn
.iter()
.flat_map(|entry| entry)
.filter_map(|(entry_id, entry_alias)| {
let alias = String::from_utf8_lossy(entry_alias.as_ref());
if aliases.contains(alias.as_ref()) {
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
Some(entry_id.to_string())
} else {
None
}
})
.collect();
Ok(ids)
}
}
}
}

View file

@ -1,11 +1,18 @@
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::{
cli::{
arg::path_parser,
printer::{OutputFmt, Printer},
},
config::TomlConfig as _,
};
use std::path::PathBuf; use std::path::PathBuf;
use crate::{ use crate::{
account::command::AccountSubcommand, account::command::AccountSubcommand,
completion::command::CompletionGenerateCommand, completion::command::CompletionGenerateCommand,
config::{self, Config}, config::TomlConfig,
envelope::command::EnvelopeSubcommand, envelope::command::EnvelopeSubcommand,
flag::command::FlagSubcommand, flag::command::FlagSubcommand,
folder::command::FolderSubcommand, folder::command::FolderSubcommand,
@ -14,8 +21,6 @@ use crate::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand, attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand, template::command::TemplateSubcommand,
}, },
output::OutputFmt,
printer::Printer,
}; };
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -34,7 +39,7 @@ pub struct Cli {
/// which allows you to separate your public config from your /// which allows you to separate your public config from your
/// private(s) one(s). /// private(s) one(s).
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")] #[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
#[arg(value_name = "PATH", value_parser = config::path_parser)] #[arg(value_name = "PATH", value_parser = path_parser)]
pub config_paths: Vec<PathBuf>, pub config_paths: Vec<PathBuf>,
/// Customize the output format. /// Customize the output format.
@ -111,31 +116,31 @@ impl HimalayaCommand {
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
match self { match self {
Self::Account(cmd) => { Self::Account(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Folder(cmd) => { Self::Folder(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Envelope(cmd) => { Self::Envelope(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Flag(cmd) => { Self::Flag(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Message(cmd) => { Self::Message(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Attachment(cmd) => { Self::Attachment(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Template(cmd) => { Self::Template(cmd) => {
let config = Config::from_paths_or_default(config_paths).await?; let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await cmd.execute(printer, &config).await
} }
Self::Manual(cmd) => cmd.execute(printer).await, Self::Manual(cmd) => cmd.execute(printer).await,

3
src/config.rs Normal file
View file

@ -0,0 +1,3 @@
use pimalaya_tui::himalaya::config::HimalayaTomlConfig;
pub type TomlConfig = HimalayaTomlConfig;

View file

@ -1,230 +0,0 @@
#[cfg(feature = "wizard")]
pub mod wizard;
use std::{collections::HashMap, path::PathBuf, sync::Arc};
use color_eyre::{eyre::eyre, Result};
use crossterm::style::Color;
use email::{
account::config::AccountConfig, envelope::config::EnvelopeConfig, folder::config::FolderConfig,
message::config::MessageConfig,
};
use pimalaya_tui::config::TomlConfig;
use serde::{Deserialize, Serialize};
use shellexpand_utils::{canonicalize, expand};
use crate::account::config::{ListAccountsTableConfig, TomlAccountConfig};
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct Config {
#[serde(alias = "name")]
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub accounts: HashMap<String, TomlAccountConfig>,
pub account: Option<AccountsConfig>,
}
impl TomlConfig<AccountConfig> for Config {
fn project_name() -> &'static str {
env!("CARGO_PKG_NAME")
}
}
impl Config {
/// 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.
#[cfg(feature = "wizard")]
async fn from_wizard(path: &PathBuf) -> Result<Self> {
Self::confirm_from_wizard(path)?;
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_paths(&[path]),
#[cfg(feature = "wizard")]
None => Self::from_wizard(&Self::default_path()?).await,
#[cfg(not(feature = "wizard"))]
None => color_eyre::eyre::bail!("cannot find config file from default paths"),
}
}
/// 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_paths_or_default(paths: &[PathBuf]) -> Result<Self> {
match paths.len() {
0 => Self::from_default_paths().await,
_ if paths[0].exists() => Self::from_paths(paths),
#[cfg(feature = "wizard")]
_ => Self::from_wizard(&paths[0]).await,
#[cfg(not(feature = "wizard"))]
_ => color_eyre::eyre::bail!("cannot find config file from default paths"),
}
}
pub fn into_toml_account_config(
&self,
account_name: Option<&str>,
) -> Result<(String, TomlAccountConfig)> {
#[allow(unused_mut)]
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)
.map(|_| (name.to_owned(), account.clone()))
})
.ok_or_else(|| eyre!("cannot find default account")),
Some(name) => self
.accounts
.get(name)
.map(|account| (name.to_owned(), account.clone()))
.ok_or_else(|| eyre!("cannot find account {name}")),
}?;
#[cfg(all(feature = "imap", feature = "keyring"))]
if let Some(imap_config) = toml_account_config.imap.as_mut() {
imap_config
.auth
.replace_undefined_keyring_entries(&account_name)?;
}
#[cfg(all(feature = "smtp", feature = "keyring"))]
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>,
) -> Result<(Arc<TomlAccountConfig>, Arc<AccountConfig>)> {
let (account_name, toml_account_config) = self.into_toml_account_config(account_name)?;
let config = email::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),
thread: c.thread.map(|c| c.remote),
}),
flag: None,
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),
delete: c.delete.map(Into::into),
}),
template: config.template,
#[cfg(feature = "pgp")]
pgp: config.pgp,
},
)
},
)),
};
let account_config = config.account(account_name)?;
Ok((Arc::new(toml_account_config), Arc::new(account_config)))
}
pub fn account_list_table_preset(&self) -> Option<String> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.preset.clone())
}
pub fn account_list_table_name_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.name_color)
}
pub fn account_list_table_backends_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.backends_color)
}
pub fn account_list_table_default_color(&self) -> Option<Color> {
self.account
.as_ref()
.and_then(|account| account.list.as_ref())
.and_then(|list| list.table.as_ref())
.and_then(|table| table.default_color)
}
}
/// 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())
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AccountsConfig {
pub list: Option<ListAccountsConfig>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ListAccountsConfig {
pub table: Option<ListAccountsTableConfig>,
}

View file

@ -1,27 +0,0 @@
use std::{fs, path::PathBuf};
use color_eyre::Result;
use pimalaya_tui::{config::TomlConfig, print, prompt};
use crate::account;
use super::Config;
pub async fn configure(path: &PathBuf) -> Result<Config> {
print::section("Configuring your default account");
let mut config = Config::default();
let (account_name, account_config) = account::wizard::configure().await?;
config.accounts.insert(account_name, account_config);
let path = prompt::path("Where to save the configuration?", Some(path))?;
println!("Writing the configuration to {}", path.display());
let toml = config.pretty_serialize()?;
fs::create_dir_all(path.parent().unwrap_or(&path))?;
fs::write(path, toml)?;
println!("Done! Exiting the wizard…");
Ok(config)
}

View file

@ -1,3 +1,5 @@
use std::{process::exit, sync::Arc};
use ariadne::{Color, Label, Report, ReportKind, Source}; use ariadne::{Color, Label, Report, ReportKind, Source};
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
@ -5,12 +7,15 @@ use email::{
backend::feature::BackendFeatureSource, email::search_query, backend::feature::BackendFeatureSource, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
}; };
use std::process::exit; use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig,
envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer, folder::arg::name::FolderNameOptionalFlag,
}; };
/// List all envelopes. /// List all envelopes.
@ -132,27 +137,32 @@ impl Default for ListEnvelopesCommand {
} }
impl ListEnvelopesCommand { impl ListEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list envelopes command"); info!("executing list envelopes command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let toml_account_config = Arc::new(toml_account_config);
let folder = &self.folder.name; let folder = &self.folder.name;
let page = 1.max(self.page) - 1; let page = 1.max(self.page) - 1;
let page_size = self let page_size = self
.page_size .page_size
.unwrap_or_else(|| account_config.get_envelope_list_page_size()); .unwrap_or_else(|| account_config.get_envelope_list_page_size());
let list_envelopes_kind = toml_account_config.list_envelopes_kind(); let backend = BackendBuilder::new(
let backend = Backend::new(
toml_account_config.clone(), toml_account_config.clone(),
account_config.clone(), Arc::new(account_config),
list_envelopes_kind, |builder| {
|builder| builder.set_list_envelopes(BackendFeatureSource::Context), builder
.without_features()
.with_list_envelopes(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let query = self let query = self

View file

@ -3,8 +3,9 @@ pub mod thread;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand}; use self::{list::ListEnvelopesCommand, thread::ThreadEnvelopesCommand};
@ -25,7 +26,7 @@ pub enum EnvelopeSubcommand {
impl EnvelopeSubcommand { impl EnvelopeSubcommand {
#[allow(unused)] #[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::List(cmd) => cmd.execute(printer, config).await, Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await, Self::Thread(cmd) => cmd.execute(printer, config).await,

View file

@ -5,12 +5,16 @@ use email::{
backend::feature::BackendFeatureSource, email::search_query, backend::feature::BackendFeatureSource, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
}; };
use std::process::exit; use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTree},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{process::exit, sync::Arc};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, envelope::EnvelopesTree, account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer, folder::arg::name::FolderNameOptionalFlag,
}; };
/// Thread all envelopes. /// Thread all envelopes.
@ -34,22 +38,27 @@ pub struct ThreadEnvelopesCommand {
} }
impl ThreadEnvelopesCommand { impl ThreadEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command"); info!("executing thread envelopes command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let account_config = Arc::new(account_config);
let folder = &self.folder.name; let folder = &self.folder.name;
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
thread_envelopes_kind, |builder| {
|builder| builder.set_thread_envelopes(BackendFeatureSource::Context), builder
.without_features()
.with_thread_envelopes(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let query = self let query = self

View file

@ -1,163 +0,0 @@
use comfy_table::presets;
use crossterm::style::Color;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::{backend::BackendKind, ui::map_color};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>,
pub thread: Option<ThreadEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>,
}
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)]
#[serde(rename_all = "kebab-case")]
pub struct ListEnvelopesConfig {
pub backend: Option<BackendKind>,
pub table: Option<ListEnvelopesTableConfig>,
#[serde(flatten)]
pub remote: email::envelope::list::config::EnvelopeListConfig,
}
impl ListEnvelopesConfig {
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)]
#[serde(rename_all = "kebab-case")]
pub struct ListEnvelopesTableConfig {
pub preset: Option<String>,
pub unseen_char: Option<char>,
pub replied_char: Option<char>,
pub flagged_char: Option<char>,
pub attachment_char: Option<char>,
pub id_color: Option<Color>,
pub flags_color: Option<Color>,
pub subject_color: Option<Color>,
pub sender_color: Option<Color>,
pub date_color: Option<Color>,
}
impl ListEnvelopesTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn replied_char(&self, replied: bool) -> char {
if replied {
self.replied_char.unwrap_or('R')
} else {
' '
}
}
pub fn flagged_char(&self, flagged: bool) -> char {
if flagged {
self.flagged_char.unwrap_or('!')
} else {
' '
}
}
pub fn attachment_char(&self, attachment: bool) -> char {
if attachment {
self.attachment_char.unwrap_or('@')
} else {
' '
}
}
pub fn unseen_char(&self, unseen: bool) -> char {
if unseen {
self.unseen_char.unwrap_or('*')
} else {
' '
}
}
pub fn id_color(&self) -> comfy_table::Color {
map_color(self.id_color.unwrap_or(Color::Red))
}
pub fn flags_color(&self) -> comfy_table::Color {
map_color(self.flags_color.unwrap_or(Color::Reset))
}
pub fn subject_color(&self) -> comfy_table::Color {
map_color(self.subject_color.unwrap_or(Color::Green))
}
pub fn sender_color(&self) -> comfy_table::Color {
map_color(self.sender_color.unwrap_or(Color::Blue))
}
pub fn date_color(&self) -> comfy_table::Color {
map_color(self.date_color.unwrap_or(Color::DarkYellow))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct ThreadEnvelopesConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::envelope::thread::config::EnvelopeThreadConfig,
}
impl ThreadEnvelopesConfig {
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 GetEnvelopeConfig {
pub backend: Option<BackendKind>,
}
impl GetEnvelopeConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,15 +1,19 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Add flag(s) to an envelope. /// Add flag(s) to an envelope.
@ -29,7 +33,7 @@ pub struct FlagAddCommand {
} }
impl FlagAddCommand { impl FlagAddCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing add flag(s) command"); info!("executing add flag(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagAddCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_flags_kind = toml_account_config.add_flags_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
add_flags_kind, .without_features()
|builder| builder.set_add_flags(BackendFeatureSource::Context), .with_add_flags(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.add_flags(folder, &ids, &flags).await?; backend.add_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully added!")) printer.out(format!("Flag(s) {flags} successfully added!\n"))
} }
} }

View file

@ -4,8 +4,9 @@ mod set;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}; use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
@ -32,7 +33,7 @@ pub enum FlagSubcommand {
impl FlagSubcommand { impl FlagSubcommand {
#[allow(unused)] #[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Add(cmd) => cmd.execute(printer, config).await, Self::Add(cmd) => cmd.execute(printer, config).await,
Self::Set(cmd) => cmd.execute(printer, config).await, Self::Set(cmd) => cmd.execute(printer, config).await,

View file

@ -1,15 +1,19 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Remove flag(s) from an envelope. /// Remove flag(s) from an envelope.
@ -29,7 +33,7 @@ pub struct FlagRemoveCommand {
} }
impl FlagRemoveCommand { impl FlagRemoveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing remove flag(s) command"); info!("executing remove flag(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagRemoveCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let remove_flags_kind = toml_account_config.remove_flags_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
remove_flags_kind, .without_features()
|builder| builder.set_remove_flags(BackendFeatureSource::Context), .with_remove_flags(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.remove_flags(folder, &ids, &flags).await?; backend.remove_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully removed!")) printer.out(format!("Flag(s) {flags} successfully removed!\n"))
} }
} }

View file

@ -1,15 +1,19 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Replace flag(s) of an envelope. /// Replace flag(s) of an envelope.
@ -29,7 +33,7 @@ pub struct FlagSetCommand {
} }
impl FlagSetCommand { impl FlagSetCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing set flag(s) command"); info!("executing set flag(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagSetCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let set_flags_kind = toml_account_config.set_flags_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
set_flags_kind, .without_features()
|builder| builder.set_set_flags(BackendFeatureSource::Context), .with_set_flags(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.set_flags(folder, &ids, &flags).await?; backend.set_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully replaced!")) printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
} }
} }

View file

@ -1,82 +0,0 @@
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
}
}

View file

@ -1,48 +1,2 @@
pub mod arg; pub mod arg;
pub mod command; 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())
}
}

View file

@ -1,432 +1,3 @@
pub mod arg; pub mod arg;
pub mod command; pub mod command;
pub mod config;
pub mod flag; pub mod flag;
use color_eyre::Result;
use comfy_table::{Attribute, Cell, ContentArrangement, Row, Table};
use crossterm::{
cursor,
style::{Color, Stylize},
terminal,
};
use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
use petgraph::graphmap::DiGraphMap;
use serde::{Serialize, Serializer};
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
use crate::{
cache::IdMapper,
flag::{Flag, Flags},
};
use self::config::ListEnvelopesTableConfig;
#[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 to: Mailbox,
pub date: String,
pub has_attachment: bool,
}
impl Envelope {
fn to_row(&self, config: &ListEnvelopesTableConfig) -> Row {
let mut all_attributes = vec![];
let unseen = !self.flags.contains(&Flag::Seen);
if unseen {
all_attributes.push(Attribute::Bold)
}
let flags = {
let mut flags = String::new();
flags.push(config.flagged_char(self.flags.contains(&Flag::Flagged)));
flags.push(config.unseen_char(unseen));
flags.push(config.attachment_char(self.has_attachment));
flags.push(config.replied_char(self.flags.contains(&Flag::Answered)));
flags
};
let mut row = Row::new();
row.max_height(1);
row.add_cell(
Cell::new(&self.id)
.add_attributes(all_attributes.clone())
.fg(config.id_color()),
)
.add_cell(
Cell::new(flags)
.add_attributes(all_attributes.clone())
.fg(config.flags_color()),
)
.add_cell(
Cell::new(&self.subject)
.add_attributes(all_attributes.clone())
.fg(config.subject_color()),
)
.add_cell(
Cell::new(if let Some(name) = &self.from.name {
name
} else {
&self.from.addr
})
.add_attributes(all_attributes.clone())
.fg(config.sender_color()),
)
.add_cell(
Cell::new(&self.date)
.add_attributes(all_attributes)
.fg(config.date_color()),
);
row
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>);
impl Envelopes {
pub fn try_from_backend(
config: &AccountConfig,
id_mapper: &IdMapper,
envelopes: email::envelope::Envelopes,
) -> Result<Envelopes> {
let envelopes = envelopes
.iter()
.map(|envelope| {
Ok(Envelope {
id: id_mapper.get_or_create_alias(&envelope.id)?,
flags: envelope.flags.clone().into(),
subject: envelope.subject.clone(),
from: Mailbox {
name: envelope.from.name.clone(),
addr: envelope.from.addr.clone(),
},
to: Mailbox {
name: envelope.to.name.clone(),
addr: envelope.to.addr.clone(),
},
date: envelope.format_date(config),
has_attachment: envelope.has_attachment,
})
})
.collect::<Result<Vec<_>>>()?;
Ok(Envelopes(envelopes))
}
}
impl Deref for Envelopes {
type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct EnvelopesTable {
envelopes: Envelopes,
width: Option<u16>,
config: ListEnvelopesTableConfig,
}
impl EnvelopesTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_unseen_char(mut self, char: Option<char>) -> Self {
self.config.unseen_char = char;
self
}
pub fn with_some_replied_char(mut self, char: Option<char>) -> Self {
self.config.replied_char = char;
self
}
pub fn with_some_flagged_char(mut self, char: Option<char>) -> Self {
self.config.flagged_char = char;
self
}
pub fn with_some_attachment_char(mut self, char: Option<char>) -> Self {
self.config.attachment_char = char;
self
}
pub fn with_some_id_color(mut self, color: Option<Color>) -> Self {
self.config.id_color = color;
self
}
pub fn with_some_flags_color(mut self, color: Option<Color>) -> Self {
self.config.flags_color = color;
self
}
pub fn with_some_subject_color(mut self, color: Option<Color>) -> Self {
self.config.subject_color = color;
self
}
pub fn with_some_sender_color(mut self, color: Option<Color>) -> Self {
self.config.sender_color = color;
self
}
pub fn with_some_date_color(mut self, color: Option<Color>) -> Self {
self.config.date_color = color;
self
}
}
impl From<Envelopes> for EnvelopesTable {
fn from(envelopes: Envelopes) -> Self {
Self {
envelopes,
width: None,
config: Default::default(),
}
}
}
impl fmt::Display for EnvelopesTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([
Cell::new("ID"),
Cell::new("FLAGS"),
Cell::new("SUBJECT"),
Cell::new("FROM"),
Cell::new("DATE"),
]))
.add_rows(self.envelopes.iter().map(|env| env.to_row(&self.config)));
if let Some(width) = self.width {
table.set_width(width);
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for EnvelopesTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.serialize(serializer)
}
}
pub struct ThreadedEnvelopes(email::envelope::ThreadedEnvelopes);
impl ThreadedEnvelopes {
pub fn try_from_backend(
id_mapper: &IdMapper,
envelopes: email::envelope::ThreadedEnvelopes,
) -> Result<ThreadedEnvelopes> {
let prev_edges = envelopes
.graph()
.all_edges()
.map(|(a, b, w)| {
let a = id_mapper.get_or_create_alias(&a.id)?;
let b = id_mapper.get_or_create_alias(&b.id)?;
Ok((a, b, *w))
})
.collect::<Result<Vec<_>>>()?;
let envelopes = envelopes
.map()
.iter()
.map(|(_, envelope)| {
let id = id_mapper.get_or_create_alias(&envelope.id)?;
let envelope = email::envelope::Envelope {
id: id.clone(),
message_id: envelope.message_id.clone(),
in_reply_to: envelope.in_reply_to.clone(),
flags: envelope.flags.clone(),
subject: envelope.subject.clone(),
from: envelope.from.clone(),
to: envelope.to.clone(),
date: envelope.date.clone(),
has_attachment: envelope.has_attachment,
};
Ok((id, envelope))
})
.collect::<Result<HashMap<_, _>>>()?;
let envelopes = email::envelope::ThreadedEnvelopes::build(envelopes, move |envelopes| {
let mut graph = DiGraphMap::<ThreadedEnvelope, u8>::new();
for (a, b, w) in prev_edges.clone() {
let eb = envelopes.get(&b).unwrap();
match envelopes.get(&a) {
Some(ea) => {
graph.add_edge(ea.as_threaded(), eb.as_threaded(), w);
}
None => {
let ea = ThreadedEnvelope {
id: "0",
message_id: "0",
subject: "",
from: "",
date: Default::default(),
};
graph.add_edge(ea, eb.as_threaded(), w);
}
}
}
graph
});
Ok(ThreadedEnvelopes(envelopes))
}
}
impl Deref for ThreadedEnvelopes {
type Target = email::envelope::ThreadedEnvelopes;
fn deref(&self) -> &Self::Target {
&self.0
}
}
pub struct EnvelopesTree {
config: Arc<AccountConfig>,
envelopes: ThreadedEnvelopes,
}
impl EnvelopesTree {
pub fn new(config: Arc<AccountConfig>, envelopes: ThreadedEnvelopes) -> Self {
Self { config, envelopes }
}
pub fn fmt(
f: &mut fmt::Formatter,
config: &AccountConfig,
graph: &DiGraphMap<ThreadedEnvelope<'_>, u8>,
parent: ThreadedEnvelope<'_>,
pad: String,
weight: u8,
) -> fmt::Result {
let edges = graph
.all_edges()
.filter_map(|(a, b, w)| {
if a == parent && *w == weight {
Some(b)
} else {
None
}
})
.collect::<Vec<_>>();
if parent.id == "0" {
f.write_str("root")?;
} else {
write!(f, "{}{}", parent.id.red(), ") ".dark_grey())?;
if !parent.subject.is_empty() {
write!(f, "{} ", parent.subject.green())?;
}
if !parent.from.is_empty() {
let left = "<".dark_grey();
let right = ">".dark_grey();
write!(f, "{left}{}{right}", parent.from.blue())?;
}
let date = parent.format_date(config);
let cursor_date_begin_col = terminal::size().unwrap().0 - date.len() as u16;
let dots =
"·".repeat((cursor_date_begin_col - cursor::position().unwrap().0 - 2) as usize);
write!(f, " {} {}", dots.dark_grey(), date.dark_yellow())?;
}
writeln!(f)?;
let edges_count = edges.len();
for (i, b) in edges.into_iter().enumerate() {
let is_last = edges_count == i + 1;
let (x, y) = if is_last {
(' ', '└')
} else {
('│', '├')
};
write!(f, "{pad}{y}─ ")?;
let pad = format!("{pad}{x} ");
Self::fmt(f, config, graph, b, pad, weight + 1)?;
}
Ok(())
}
}
impl fmt::Display for EnvelopesTree {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
EnvelopesTree::fmt(
f,
&self.config,
self.envelopes.0.graph(),
ThreadedEnvelope {
id: "0",
message_id: "0",
from: "",
subject: "",
date: Default::default(),
},
String::new(),
0,
)
}
}
impl Serialize for EnvelopesTree {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.envelopes.0.serialize(serializer)
}
}
impl Deref for EnvelopesTree {
type Target = ThreadedEnvelopes;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}

View file

@ -1,14 +1,17 @@
use clap::Parser; use clap::Parser;
use color_eyre::{eyre::Context, Result}; use color_eyre::{eyre::Context, Result};
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use std::{fs, path::PathBuf}; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{fs, path::PathBuf, sync::Arc};
use tracing::info; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Download all attachments for the given message. /// Download all attachments for the given message.
@ -28,7 +31,7 @@ pub struct AttachmentDownloadCommand {
} }
impl AttachmentDownloadCommand { impl AttachmentDownloadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing download attachment(s) command"); info!("executing download attachment(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -38,14 +41,19 @@ impl AttachmentDownloadCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
get_messages_kind, |builder| {
|builder| builder.set_get_messages(BackendFeatureSource::Context), builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let emails = backend.get_messages(folder, ids).await?; let emails = backend.get_messages(folder, ids).await?;

View file

@ -2,8 +2,9 @@ mod download;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::download::AttachmentDownloadCommand; use self::download::AttachmentDownloadCommand;
@ -19,7 +20,7 @@ pub enum AttachmentSubcommand {
} }
impl AttachmentSubcommand { impl AttachmentSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Download(cmd) => cmd.execute(printer, config).await, Self::Download(cmd) => cmd.execute(printer, config).await,
} }

View file

@ -1,15 +1,19 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdsArgs, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
}; };
/// Copy a message from a source folder to a target folder. /// Copy a message from a source folder to a target folder.
@ -29,7 +33,7 @@ pub struct MessageCopyCommand {
} }
impl MessageCopyCommand { impl MessageCopyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing copy message(s) command"); info!("executing copy message(s) command");
let source = &self.source_folder.name; let source = &self.source_folder.name;
@ -40,20 +44,23 @@ impl MessageCopyCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let copy_messages_kind = toml_account_config.copy_messages_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
copy_messages_kind, .without_features()
|builder| builder.set_copy_messages(BackendFeatureSource::Context), .with_copy_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.copy_messages(source, target, ids).await?; backend.copy_messages(source, target, ids).await?;
printer.out(format!( printer.out(format!(
"Message(s) successfully copied from {source} to {target}!" "Message(s) successfully copied from {source} to {target}!\n"
)) ))
} }
} }

View file

@ -1,12 +1,17 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Mark as deleted a message from a folder. /// Mark as deleted a message from a folder.
@ -28,7 +33,7 @@ pub struct MessageDeleteCommand {
} }
impl MessageDeleteCommand { impl MessageDeleteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing delete message(s) command"); info!("executing delete message(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -38,18 +43,21 @@ impl MessageDeleteCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let delete_messages_kind = toml_account_config.delete_messages_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
delete_messages_kind, .without_features()
|builder| builder.set_delete_messages(BackendFeatureSource::Context), .with_delete_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.delete_messages(folder, ids).await?; backend.delete_messages(folder, ids).await?;
printer.out(format!("Message(s) successfully removed from {folder}!")) printer.out(format!("Message(s) successfully removed from {folder}!\n"))
} }
} }

View file

@ -1,17 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::{eyre::eyre, Result}; use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdArg, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
}; };
/// Forward a message. /// Forward a message.
@ -39,7 +42,7 @@ pub struct MessageForwardCommand {
} }
impl MessageForwardCommand { impl MessageForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing forward message command"); info!("executing forward message command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -48,18 +51,20 @@ impl MessageForwardCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let account_config = Arc::new(account_config);
let send_message_kind = toml_account_config.send_message_kind();
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| { |builder| {
builder.set_add_message(BackendFeatureSource::Context); builder
builder.set_send_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.without_sending_backend()
.build()
.await?; .await?;
let id = self.envelope.id; let id = self.envelope.id;

View file

@ -1,14 +1,17 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use mail_builder::MessageBuilder; use mail_builder::MessageBuilder;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use url::Url; use url::Url;
use crate::{ use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
account::arg::name::AccountNameFlag, backend::Backend, config::Config, printer::Printer,
ui::editor,
};
/// Parse and edit a message from a mailto URL string. /// Parse and edit a message from a mailto URL string.
/// ///
@ -34,25 +37,27 @@ impl MessageMailtoCommand {
}) })
} }
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing mailto message command"); info!("executing mailto message command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let account_config = Arc::new(account_config);
let send_message_kind = toml_account_config.send_message_kind();
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| { |builder| {
builder.set_add_message(BackendFeatureSource::Context); builder
builder.set_send_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.without_sending_backend()
.build()
.await?; .await?;
let mut builder = MessageBuilder::new().to(self.url.path()); let mut builder = MessageBuilder::new().to(self.url.path());

View file

@ -12,8 +12,9 @@ pub mod write;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{ use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand, copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
@ -67,7 +68,7 @@ pub enum MessageSubcommand {
impl MessageSubcommand { impl MessageSubcommand {
#[allow(unused)] #[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Read(cmd) => cmd.execute(printer, config).await, Self::Read(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await, Self::Thread(cmd) => cmd.execute(printer, config).await,

View file

@ -1,16 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
#[allow(unused)] #[allow(unused)]
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdsArgs, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
}; };
/// Move a message from a source folder to a target folder. /// Move a message from a source folder to a target folder.
@ -30,7 +34,7 @@ pub struct MessageMoveCommand {
} }
impl MessageMoveCommand { impl MessageMoveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing move message(s) command"); info!("executing move message(s) command");
let source = &self.source_folder.name; let source = &self.source_folder.name;
@ -41,20 +45,23 @@ impl MessageMoveCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let move_messages_kind = toml_account_config.move_messages_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
move_messages_kind, .without_features()
|builder| builder.set_move_messages(BackendFeatureSource::Context), .with_move_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.move_messages(source, target, ids).await?; backend.move_messages(source, target, ids).await?;
printer.out(format!( printer.out(format!(
"Message(s) successfully moved from {source} to {target}!" "Message(s) successfully moved from {source} to {target}!\n"
)) ))
} }
} }

View file

@ -1,14 +1,19 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts; use mml::message::FilterParts;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
#[allow(unused)] #[allow(unused)]
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Read a message. /// Read a message.
@ -73,7 +78,7 @@ pub struct MessageReadCommand {
} }
impl MessageReadCommand { impl MessageReadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing read message(s) command"); info!("executing read message(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -83,14 +88,19 @@ impl MessageReadCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
get_messages_kind, |builder| {
|builder| builder.set_get_messages(BackendFeatureSource::Context), builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let emails = if self.preview { let emails = if self.preview {

View file

@ -1,17 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::{eyre::eyre, Result}; use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdArg, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
printer::Printer,
ui::editor,
}; };
/// Reply to a message. /// Reply to a message.
@ -42,7 +45,7 @@ pub struct MessageReplyCommand {
} }
impl MessageReplyCommand { impl MessageReplyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing reply message command"); info!("executing reply message command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -50,18 +53,19 @@ impl MessageReplyCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let account_config = Arc::new(account_config);
let send_message_kind = toml_account_config.send_message_kind();
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| { |builder| {
builder.set_add_message(BackendFeatureSource::Context); builder
builder.set_send_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.build()
.await?; .await?;
let id = self.envelope.id; let id = self.envelope.id;

View file

@ -1,13 +1,19 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal}; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info; use tracing::info;
#[allow(unused)]
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer, folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
}; };
/// Save a message to a folder. /// Save a message to a folder.
@ -26,7 +32,7 @@ pub struct MessageSaveCommand {
} }
impl MessageSaveCommand { impl MessageSaveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing save message command"); info!("executing save message command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -35,14 +41,17 @@ impl MessageSaveCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
add_message_kind, .without_features()
|builder| builder.set_add_message(BackendFeatureSource::Context), .with_add_message(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let is_tty = io::stdin().is_terminal(); let is_tty = io::stdin().is_terminal();
@ -60,6 +69,6 @@ impl MessageSaveCommand {
backend.add_message(folder, msg.as_bytes()).await?; backend.add_message(folder, msg.as_bytes()).await?;
printer.out(format!("Message successfully saved to {folder}!")) printer.out(format!("Message successfully saved to {folder}!\n"))
} }
} }

View file

@ -1,13 +1,17 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal}; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info; use tracing::info;
use crate::{ use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
message::arg::MessageRawArg, printer::Printer,
};
/// Send a message. /// Send a message.
/// ///
@ -23,28 +27,24 @@ pub struct MessageSendCommand {
} }
impl MessageSendCommand { impl MessageSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send message command"); info!("executing send message command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain( let backend = BackendBuilder::new(
toml_account_config Arc::new(toml_account_config),
.add_message_kind() Arc::new(account_config),
.filter(|_| account_config.should_save_copy_sent_message()),
);
let backend = Backend::new(
toml_account_config.clone(),
account_config,
send_message_kind,
|builder| { |builder| {
builder.set_send_message(BackendFeatureSource::Context); builder
builder.set_add_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.build()
.await?; .await?;
let msg = if io::stdin().is_terminal() { let msg = if io::stdin().is_terminal() {

View file

@ -1,15 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts; use mml::message::FilterParts;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::envelope::arg::ids::EnvelopeIdArg; use crate::envelope::arg::ids::EnvelopeIdArg;
#[allow(unused)] #[allow(unused)]
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Thread a message. /// Thread a message.
@ -74,7 +79,7 @@ pub struct MessageThreadCommand {
} }
impl MessageThreadCommand { impl MessageThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread message(s) command"); info!("executing thread message(s) command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -84,17 +89,20 @@ impl MessageThreadCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
get_messages_kind,
|builder| { |builder| {
builder.set_thread_envelopes(BackendFeatureSource::Context); builder
builder.set_get_messages(BackendFeatureSource::Context); .without_features()
.with_get_messages(BackendFeatureSource::Context)
.with_thread_envelopes(BackendFeatureSource::Context)
}, },
) )
.without_sending_backend()
.build()
.await?; .await?;
let envelopes = backend let envelopes = backend

View file

@ -1,15 +1,18 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, message::Message}; use email::{backend::feature::BackendFeatureSource, message::Message};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
}; };
/// Write a new message. /// Write a new message.
@ -31,25 +34,26 @@ pub struct MessageWriteCommand {
} }
impl MessageWriteCommand { impl MessageWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write message command"); info!("executing write message command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let account_config = Arc::new(account_config);
let send_message_kind = toml_account_config.send_message_kind();
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| { |builder| {
builder.set_add_message(BackendFeatureSource::Context); builder
builder.set_send_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.build()
.await?; .await?;
let tpl = Message::new_tpl_builder(account_config.clone()) let tpl = Message::new_tpl_builder(account_config.clone())

View file

@ -1,185 +0,0 @@
use email::message::delete::config::DeleteMessageStyle;
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>,
pub r#move: Option<MessageMoveConfig>,
pub delete: Option<DeleteMessageConfig>,
}
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.r#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::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::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
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct DeleteMessageConfig {
pub backend: Option<BackendKind>,
pub style: Option<DeleteMessageStyle>,
}
impl From<DeleteMessageConfig> for email::message::delete::config::DeleteMessageConfig {
fn from(config: DeleteMessageConfig) -> Self {
Self {
style: config.style,
}
}
}
impl DeleteMessageConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,5 +1,4 @@
pub mod arg; pub mod arg;
pub mod attachment; pub mod attachment;
pub mod command; pub mod command;
pub mod config;
pub mod template; pub mod template;

View file

@ -1,16 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::{eyre::eyre, Result}; use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdArg, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
}; };
/// Generate a template for forwarding a message. /// Generate a template for forwarding a message.
@ -37,7 +41,7 @@ pub struct TemplateForwardCommand {
} }
impl TemplateForwardCommand { impl TemplateForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing forward template command"); info!("executing forward template command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -46,14 +50,19 @@ impl TemplateForwardCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
get_messages_kind, |builder| {
|builder| builder.set_get_messages(BackendFeatureSource::Context), builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let id = self.envelope.id; let id = self.envelope.id;

View file

@ -6,8 +6,9 @@ mod write;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{ use self::{
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand, forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
@ -43,7 +44,7 @@ pub enum TemplateSubcommand {
} }
impl TemplateSubcommand { impl TemplateSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Write(cmd) => cmd.execute(printer, config).await, Self::Write(cmd) => cmd.execute(printer, config).await,
Self::Reply(cmd) => cmd.execute(printer, config).await, Self::Reply(cmd) => cmd.execute(printer, config).await,

View file

@ -1,16 +1,20 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::{eyre::eyre, Result}; use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, account::arg::name::AccountNameFlag,
backend::Backend, config::TomlConfig,
config::Config,
envelope::arg::ids::EnvelopeIdArg, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
printer::Printer,
}; };
/// Generate a template for replying to a message. /// Generate a template for replying to a message.
@ -41,7 +45,7 @@ pub struct TemplateReplyCommand {
} }
impl TemplateReplyCommand { impl TemplateReplyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing reply template command"); info!("executing reply template command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -51,14 +55,19 @@ impl TemplateReplyCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let get_messages_kind = toml_account_config.get_messages_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
get_messages_kind, |builder| {
|builder| builder.set_get_messages(BackendFeatureSource::Context), builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let tpl = backend let tpl = backend

View file

@ -2,13 +2,19 @@ use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use mml::MmlCompilerBuilder; use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal}; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
}; };
/// Save a template to a folder. /// Save a template to a folder.
@ -30,7 +36,7 @@ pub struct TemplateSaveCommand {
} }
impl TemplateSaveCommand { impl TemplateSaveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing save template command"); info!("executing save template command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -39,14 +45,19 @@ impl TemplateSaveCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind(); let account_config = Arc::new(account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
add_message_kind, |builder| {
|builder| builder.set_add_message(BackendFeatureSource::Context), builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let is_tty = io::stdin().is_terminal(); let is_tty = io::stdin().is_terminal();
@ -72,6 +83,6 @@ impl TemplateSaveCommand {
backend.add_message(folder, &msg).await?; backend.add_message(folder, &msg).await?;
printer.out(format!("Template successfully saved to {folder}!")) printer.out(format!("Template successfully saved to {folder}!\n"))
} }
} }

View file

@ -2,12 +2,18 @@ use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::backend::feature::BackendFeatureSource; use email::backend::feature::BackendFeatureSource;
use mml::MmlCompilerBuilder; use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal}; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
email::template::arg::TemplateRawArg, printer::Printer,
}; };
/// Send a template. /// Send a template.
@ -26,28 +32,26 @@ pub struct TemplateSendCommand {
} }
impl TemplateSendCommand { impl TemplateSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send template command"); info!("executing send template command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain( let account_config = Arc::new(account_config);
toml_account_config
.add_message_kind()
.filter(|_| account_config.should_save_copy_sent_message()),
);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), Arc::new(toml_account_config),
account_config.clone(), account_config.clone(),
send_message_kind,
|builder| { |builder| {
builder.set_send_message(BackendFeatureSource::Context); builder
builder.set_add_message(BackendFeatureSource::Context); .without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
}, },
) )
.build()
.await?; .await?;
let tpl = if io::stdin().is_terminal() { let tpl = if io::stdin().is_terminal() {

View file

@ -1,12 +1,14 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::message::Message; use email::message::Message;
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig,
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs, email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
printer::Printer,
}; };
/// Generate a template for writing a new message from scratch. /// Generate a template for writing a new message from scratch.
@ -26,14 +28,14 @@ pub struct TemplateWriteCommand {
} }
impl TemplateWriteCommand { impl TemplateWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write template command"); info!("executing write template command");
let (_, account_config) = config let (_, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let tpl = Message::new_tpl_builder(account_config) let tpl = Message::new_tpl_builder(Arc::new(account_config))
.with_headers(self.headers.raw) .with_headers(self.headers.raw)
.with_body(self.body.raw()) .with_body(self.body.raw())
.build() .build()

View file

@ -1,11 +1,16 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder}; use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
folder::arg::name::FolderNameArg, printer::Printer,
}; };
/// Create a new folder. /// Create a new folder.
@ -22,26 +27,30 @@ pub struct AddFolderCommand {
} }
impl AddFolderCommand { impl AddFolderCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing create folder command"); info!("executing create folder command");
let folder = &self.folder.name; let folder = &self.folder.name;
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let add_folder_kind = toml_account_config.add_folder_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
add_folder_kind, .without_features()
|builder| builder.set_add_folder(BackendFeatureSource::Context), .with_add_folder(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.add_folder(folder).await?; backend.add_folder(folder).await?;
printer.log(format!("Folder {folder} successfully created!")) printer.out(format!("Folder {folder} successfully created!\n"))
} }
} }

View file

@ -1,14 +1,16 @@
use std::process; use std::{process, sync::Arc};
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder}; use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
use pimalaya_tui::prompt; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
folder::arg::name::FolderNameArg, printer::Printer,
}; };
/// Delete a folder. /// Delete a folder.
@ -25,12 +27,13 @@ pub struct FolderDeleteCommand {
} }
impl FolderDeleteCommand { impl FolderDeleteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing delete folder command"); info!("executing delete folder command");
let folder = &self.folder.name; let folder = &self.folder.name;
let confirm = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted."); let confirm = format!("Do you really want to delete the folder {folder}");
let confirm = format!("{confirm}? All emails will be definitely deleted.");
if !prompt::bool(confirm, false)? { if !prompt::bool(confirm, false)? {
process::exit(0); process::exit(0);
@ -40,18 +43,21 @@ impl FolderDeleteCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let delete_folder_kind = toml_account_config.delete_folder_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
delete_folder_kind, .without_features()
|builder| builder.set_delete_folder(BackendFeatureSource::Context), .with_delete_folder(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.delete_folder(folder).await?; backend.delete_folder(folder).await?;
printer.log(format!("Folder {folder} successfully deleted!")) printer.out(format!("Folder {folder} successfully deleted!\n"))
} }
} }

View file

@ -1,11 +1,16 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder}; use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
folder::arg::name::FolderNameArg, printer::Printer,
}; };
/// Expunge a folder. /// Expunge a folder.
@ -23,7 +28,7 @@ pub struct FolderExpungeCommand {
} }
impl FolderExpungeCommand { impl FolderExpungeCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing expunge folder command"); info!("executing expunge folder command");
let folder = &self.folder.name; let folder = &self.folder.name;
@ -31,18 +36,21 @@ impl FolderExpungeCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let expunge_folder_kind = toml_account_config.expunge_folder_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
expunge_folder_kind, .without_features()
|builder| builder.set_expunge_folder(BackendFeatureSource::Context), .with_expunge_folder(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.expunge_folder(folder).await?; backend.expunge_folder(folder).await?;
printer.log(format!("Folder {folder} successfully expunged!")) printer.out(format!("Folder {folder} successfully expunged!\n"))
} }
} }

View file

@ -1,15 +1,18 @@
use std::sync::Arc;
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders}; use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders};
use pimalaya_tui::{
himalaya::{
backend::BackendBuilder,
config::{Folders, FoldersTable},
},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
folder::{Folders, FoldersTable},
printer::Printer,
};
/// List all folders. /// List all folders.
/// ///
@ -29,21 +32,26 @@ pub struct FolderListCommand {
} }
impl FolderListCommand { impl FolderListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list folders command"); info!("executing list folders command");
let (toml_account_config, account_config) = config let (toml_account_config, account_config) = config
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let list_folders_kind = toml_account_config.list_folders_kind(); let toml_account_config = Arc::new(toml_account_config);
let backend = Backend::new( let backend = BackendBuilder::new(
toml_account_config.clone(), toml_account_config.clone(),
account_config.clone(), Arc::new(account_config),
list_folders_kind, |builder| {
|builder| builder.set_list_folders(BackendFeatureSource::Context), builder
.without_features()
.with_list_folders(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
let folders = Folders::from(backend.list_folders().await?); let folders = Folders::from(backend.list_folders().await?);

View file

@ -6,8 +6,9 @@ mod purge;
use clap::Subcommand; use clap::Subcommand;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer}; use crate::config::TomlConfig;
use self::{ use self::{
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
@ -38,7 +39,7 @@ pub enum FolderSubcommand {
impl FolderSubcommand { impl FolderSubcommand {
#[allow(unused)] #[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self { match self {
Self::Add(cmd) => cmd.execute(printer, config).await, Self::Add(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, config).await, Self::List(cmd) => cmd.execute(printer, config).await,

View file

@ -1,14 +1,16 @@
use std::process; use std::{process, sync::Arc};
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder}; use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
use pimalaya_tui::prompt; use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
};
use tracing::info; use tracing::info;
use crate::{ use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
folder::arg::name::FolderNameArg, printer::Printer,
}; };
/// Purge a folder. /// Purge a folder.
@ -25,12 +27,13 @@ pub struct FolderPurgeCommand {
} }
impl FolderPurgeCommand { impl FolderPurgeCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &Config) -> Result<()> { pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing purge folder command"); info!("executing purge folder command");
let folder = &self.folder.name; let folder = &self.folder.name;
let confirm = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted."); let confirm = format!("Do you really want to purge the folder {folder}");
let confirm = format!("{confirm}? All emails will be definitely deleted.");
if !prompt::bool(confirm, false)? { if !prompt::bool(confirm, false)? {
process::exit(0); process::exit(0);
@ -40,18 +43,21 @@ impl FolderPurgeCommand {
.clone() .clone()
.into_account_configs(self.account.name.as_deref())?; .into_account_configs(self.account.name.as_deref())?;
let purge_folder_kind = toml_account_config.purge_folder_kind(); let backend = BackendBuilder::new(
Arc::new(toml_account_config),
let backend = Backend::new( Arc::new(account_config),
toml_account_config.clone(), |builder| {
account_config, builder
purge_folder_kind, .without_features()
|builder| builder.set_purge_folder(BackendFeatureSource::Context), .with_purge_folder(BackendFeatureSource::Context)
},
) )
.without_sending_backend()
.build()
.await?; .await?;
backend.purge_folder(folder).await?; backend.purge_folder(folder).await?;
printer.log(format!("Folder {folder} successfully purged!")) printer.out(format!("Folder {folder} successfully purged!\n"))
} }
} }

View file

@ -1,157 +0,0 @@
use comfy_table::presets;
use crossterm::style::Color;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use crate::{backend::BackendKind, ui::map_color};
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderConfig {
#[serde(alias = "aliases")]
pub alias: Option<HashMap<String, String>>,
pub add: Option<FolderAddConfig>,
pub list: Option<FolderListConfig>,
pub expunge: Option<FolderExpungeConfig>,
pub purge: Option<FolderPurgeConfig>,
pub delete: Option<FolderDeleteConfig>,
}
impl FolderConfig {
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(list) = &self.list {
kinds.extend(list.get_used_backends());
}
if let Some(expunge) = &self.expunge {
kinds.extend(expunge.get_used_backends());
}
if let Some(purge) = &self.purge {
kinds.extend(purge.get_used_backends());
}
if let Some(delete) = &self.delete {
kinds.extend(delete.get_used_backends());
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderAddConfig {
pub backend: Option<BackendKind>,
}
impl FolderAddConfig {
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)]
#[serde(rename_all = "kebab-case")]
pub struct FolderListConfig {
pub backend: Option<BackendKind>,
pub table: Option<ListFoldersTableConfig>,
#[serde(flatten)]
pub remote: email::folder::list::config::FolderListConfig,
}
impl FolderListConfig {
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)]
#[serde(rename_all = "kebab-case")]
pub struct ListFoldersTableConfig {
pub preset: Option<String>,
pub name_color: Option<Color>,
pub desc_color: Option<Color>,
}
impl ListFoldersTableConfig {
pub fn preset(&self) -> &str {
self.preset.as_deref().unwrap_or(presets::ASCII_MARKDOWN)
}
pub fn name_color(&self) -> comfy_table::Color {
map_color(self.name_color.unwrap_or(Color::Blue))
}
pub fn desc_color(&self) -> comfy_table::Color {
map_color(self.desc_color.unwrap_or(Color::Green))
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderExpungeConfig {
pub backend: Option<BackendKind>,
}
impl FolderExpungeConfig {
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 FolderPurgeConfig {
pub backend: Option<BackendKind>,
}
impl FolderPurgeConfig {
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 FolderDeleteConfig {
pub backend: Option<BackendKind>,
}
impl FolderDeleteConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,126 +1,2 @@
pub mod arg; pub mod arg;
pub mod command; pub mod command;
pub mod config;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use crossterm::style::Color;
use serde::{Serialize, Serializer};
use std::{fmt, ops::Deref};
use self::config::ListFoldersTableConfig;
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
pub name: String,
pub desc: String,
}
impl Folder {
pub fn to_row(&self, config: &ListFoldersTableConfig) -> Row {
let mut row = Row::new();
row.max_height(1);
row.add_cell(Cell::new(&self.name).fg(config.name_color()));
row.add_cell(Cell::new(&self.desc).fg(config.desc_color()));
row
}
}
impl From<email::folder::Folder> for Folder {
fn from(folder: email::folder::Folder) -> Self {
Folder {
name: folder.name,
desc: folder.desc,
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl 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.into_iter().map(Folder::from).collect())
}
}
pub struct FoldersTable {
folders: Folders,
width: Option<u16>,
config: ListFoldersTableConfig,
}
impl FoldersTable {
pub fn with_some_width(mut self, width: Option<u16>) -> Self {
self.width = width;
self
}
pub fn with_some_preset(mut self, preset: Option<String>) -> Self {
self.config.preset = preset;
self
}
pub fn with_some_name_color(mut self, color: Option<Color>) -> Self {
self.config.name_color = color;
self
}
pub fn with_some_desc_color(mut self, color: Option<Color>) -> Self {
self.config.desc_color = color;
self
}
}
impl From<Folders> for FoldersTable {
fn from(folders: Folders) -> Self {
Self {
folders,
width: None,
config: Default::default(),
}
}
}
impl fmt::Display for FoldersTable {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut table = Table::new();
table
.load_preset(self.config.preset())
.set_content_arrangement(ContentArrangement::DynamicFullWidth)
.set_header(Row::from([Cell::new("NAME"), Cell::new("DESC")]))
.add_rows(
self.folders
.iter()
.map(|folder| folder.to_row(&self.config)),
);
if let Some(width) = self.width {
table.set_width(width);
}
writeln!(f)?;
write!(f, "{table}")?;
writeln!(f)?;
Ok(())
}
}
impl Serialize for FoldersTable {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.folders.serialize(serializer)
}
}

View file

@ -1,15 +1,10 @@
pub mod account; pub mod account;
pub mod backend;
pub mod cache;
pub mod cli; pub mod cli;
pub mod completion; pub mod completion;
pub mod config; pub mod config;
pub mod email; pub mod email;
pub mod folder; pub mod folder;
pub mod manual; pub mod manual;
pub mod output;
pub mod printer;
pub mod ui;
#[doc(inline)] #[doc(inline)]
pub use crate::email::{envelope, flag, message}; pub use crate::email::{envelope, flag, message};

View file

@ -1,10 +1,13 @@
use clap::Parser; use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use himalaya::{ use himalaya::{
cli::Cli, config::Config, envelope::command::list::ListEnvelopesCommand, cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand,
message::command::mailto::MessageMailtoCommand, printer::StdoutPrinter, message::command::mailto::MessageMailtoCommand,
};
use pimalaya_tui::terminal::{
cli::{printer::StdoutPrinter, tracing},
config::TomlConfig as _,
}; };
use pimalaya_tui::cli::tracing;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
@ -21,7 +24,7 @@ async fn main() -> Result<()> {
if let Some(ref url) = mailto { if let Some(ref url) = mailto {
let mut printer = StdoutPrinter::default(); let mut printer = StdoutPrinter::default();
let config = Config::from_default_paths().await?; let config = TomlConfig::from_default_paths().await?;
return MessageMailtoCommand::new(url)? return MessageMailtoCommand::new(url)?
.execute(&mut printer, &config) .execute(&mut printer, &config)
@ -33,7 +36,7 @@ async fn main() -> Result<()> {
let res = match cli.command { let res = match cli.command {
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
None => { None => {
let config = Config::from_paths_or_default(cli.config_paths.as_ref()).await?; let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
ListEnvelopesCommand::default() ListEnvelopesCommand::default()
.execute(&mut printer, &config) .execute(&mut printer, &config)
.await .await

View file

@ -1,11 +1,12 @@
use clap::{CommandFactory, Parser}; use clap::{CommandFactory, Parser};
use clap_mangen::Man; use clap_mangen::Man;
use color_eyre::Result; use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use shellexpand_utils::{canonicalize, expand}; use shellexpand_utils::{canonicalize, expand};
use std::{fs, path::PathBuf}; use std::{fs, path::PathBuf};
use tracing::info; use tracing::info;
use crate::{cli::Cli, printer::Printer}; use crate::cli::Cli;
/// Generate manual pages to a directory. /// Generate manual pages to a directory.
/// ///

View file

@ -1,48 +0,0 @@
//! Module related to output CLI.
//!
//! This module provides arguments related to output.
use clap::Arg;
pub(crate) const ARG_COLOR: &str = "color";
pub(crate) const ARG_OUTPUT: &str = "output";
/// Output arguments.
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[
Arg::new(ARG_OUTPUT)
.help("Define the output format")
.long("output")
.short('o')
.global(true)
.value_name("format")
.value_parser(["plain", "json"])
.default_value("plain"),
Arg::new(ARG_COLOR)
.help("Control when to use colors")
.long_help(
"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 for this flag are:
never Colors will never be used.
auto The default. himalaya tries to be smart.
always Colors will always be used regardless of where output is sent.
ansi Like 'always', but emits ANSI escapes (even in a Windows console).",
)
.long("color")
.short('C')
.global(true)
.value_parser(["never", "auto", "always", "ansi"])
.default_value("auto")
.value_name("mode"),
]
}

View file

@ -1,4 +0,0 @@
#[allow(clippy::module_inception)]
pub mod output;
pub use output::*;

View file

@ -1,49 +0,0 @@
use clap::ValueEnum;
use color_eyre::{
eyre::{bail, Error},
Result,
};
use serde::Serialize;
use std::{fmt, str::FromStr};
/// Represents the available output formats.
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, ValueEnum)]
pub enum OutputFmt {
#[default]
Plain,
Json,
}
impl FromStr for OutputFmt {
type Err = Error;
fn from_str(fmt: &str) -> Result<Self, Self::Err> {
match fmt {
fmt if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
fmt if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
unknown => bail!("cannot parse output format {unknown}"),
}
}
}
impl fmt::Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fmt = match *self {
OutputFmt::Json => "JSON",
OutputFmt::Plain => "Plain",
};
write!(f, "{}", fmt)
}
}
/// Defines a struct-wrapper to provide a JSON output.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
pub struct OutputJson<T: Serialize> {
response: T,
}
impl<T: Serialize> OutputJson<T> {
pub fn new(response: T) -> Self {
Self { response }
}
}

View file

@ -1,74 +0,0 @@
use std::{
fmt,
io::{stderr, stdout, Stderr, Stdout, Write},
};
use color_eyre::{eyre::Context, Result};
use crate::output::OutputFmt;
pub trait PrintTable {
fn print(&self, writer: &mut dyn Write, table_max_width: Option<u16>) -> Result<()>;
}
pub trait Printer {
fn out<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn log<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
self.out(data)
}
fn is_json(&self) -> bool {
false
}
}
pub struct StdoutPrinter {
stdout: Stdout,
stderr: Stderr,
output: OutputFmt,
}
impl StdoutPrinter {
pub fn new(output: OutputFmt) -> Self {
Self {
stdout: stdout(),
stderr: stderr(),
output,
}
}
}
impl Default for StdoutPrinter {
fn default() -> Self {
Self::new(Default::default())
}
}
impl Printer for StdoutPrinter {
fn out<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
match self.output {
OutputFmt::Plain => {
write!(self.stdout, "{data}")?;
}
OutputFmt::Json => {
serde_json::to_writer(&mut self.stdout, &data)
.context("cannot write json to writer")?;
}
};
Ok(())
}
fn log<T: fmt::Display + serde::Serialize>(&mut self, data: T) -> Result<()> {
if let OutputFmt::Plain = self.output {
write!(&mut self.stderr, "{data}")?;
}
Ok(())
}
fn is_json(&self) -> bool {
self.output == OutputFmt::Json
}
}

View file

@ -1,84 +0,0 @@
use std::fmt;
use color_eyre::Result;
use pimalaya_tui::prompt;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PreEditChoice {
Edit,
Discard,
Quit,
}
impl fmt::Display for PreEditChoice {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Edit => "Edit it",
Self::Discard => "Discard it",
Self::Quit => "Quit",
}
)
}
}
static PRE_EDIT_CHOICES: [PreEditChoice; 3] = [
PreEditChoice::Edit,
PreEditChoice::Discard,
PreEditChoice::Quit,
];
pub fn pre_edit() -> Result<PreEditChoice> {
let user_choice = prompt::item(
"A draft was found, what would you like to do with it?",
&PRE_EDIT_CHOICES,
None,
)?;
Ok(user_choice.clone())
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PostEditChoice {
Send,
Edit,
LocalDraft,
RemoteDraft,
Discard,
}
impl fmt::Display for PostEditChoice {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Self::Send => "Send it",
Self::Edit => "Edit it again",
Self::LocalDraft => "Save it as local draft",
Self::RemoteDraft => "Save it as remote draft",
Self::Discard => "Discard it",
}
)
}
}
static POST_EDIT_CHOICES: [PostEditChoice; 5] = [
PostEditChoice::Send,
PostEditChoice::Edit,
PostEditChoice::LocalDraft,
PostEditChoice::RemoteDraft,
PostEditChoice::Discard,
];
pub fn post_edit() -> Result<PostEditChoice> {
let user_choice = prompt::item(
"What would you like to do with this message?",
&POST_EDIT_CHOICES,
None,
)?;
Ok(user_choice.clone())
}

View file

@ -1,140 +0,0 @@
use std::{env, fs, sync::Arc};
use color_eyre::{eyre::Context, Result};
use email::{
account::config::AccountConfig,
email::utils::{local_draft_path, remove_local_draft},
flag::{Flag, Flags},
folder::DRAFTS,
template::Template,
};
use mml::MmlCompilerBuilder;
use process::SingleCommand;
use tracing::debug;
use crate::{
backend::Backend,
printer::Printer,
ui::choice::{self, PostEditChoice, PreEditChoice},
};
pub async fn open_with_tpl(tpl: Template) -> Result<Template> {
let path = local_draft_path();
debug!("create draft");
fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?;
debug!("open editor");
let editor = env::var("EDITOR").context("cannot get editor from env var")?;
SingleCommand::from(format!("{editor} {}", &path.to_string_lossy()))
.with_output_piped(false)
.run()
.await
.context("cannot launch editor")?;
debug!("read draft");
let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
Ok(content.into())
}
pub async fn open_with_local_draft() -> Result<Template> {
let path = local_draft_path();
let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
open_with_tpl(content.into()).await
}
#[allow(unused)]
pub async fn edit_tpl_with_editor<P: Printer>(
config: Arc<AccountConfig>,
printer: &mut P,
backend: &Backend,
mut tpl: Template,
) -> Result<()> {
let draft = local_draft_path();
if draft.exists() {
loop {
match choice::pre_edit() {
Ok(choice) => match choice {
PreEditChoice::Edit => {
tpl = open_with_local_draft().await?;
break;
}
PreEditChoice::Discard => {
tpl = open_with_tpl(tpl).await?;
break;
}
PreEditChoice::Quit => return Ok(()),
},
Err(err) => {
println!("{}", err);
continue;
}
}
}
} else {
tpl = open_with_tpl(tpl).await?;
}
loop {
match choice::post_edit() {
Ok(PostEditChoice::Send) => {
printer.log("Sending email…")?;
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(feature = "pgp")]
compiler.set_some_pgp(config.pgp.clone());
let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
backend.send_message_then_save_copy(&email).await?;
remove_local_draft()?;
printer.log("Done!")?;
break;
}
Ok(PostEditChoice::Edit) => {
tpl = open_with_tpl(tpl).await?;
continue;
}
Ok(PostEditChoice::LocalDraft) => {
printer.log("Email successfully saved locally")?;
break;
}
Ok(PostEditChoice::RemoteDraft) => {
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(feature = "pgp")]
compiler.set_some_pgp(config.pgp.clone());
let email = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
backend
.add_message_with_flags(
DRAFTS,
&email,
&Flags::from_iter([Flag::Seen, Flag::Draft]),
)
.await?;
remove_local_draft()?;
printer.log("Email successfully saved to drafts")?;
break;
}
Ok(PostEditChoice::Discard) => {
remove_local_draft()?;
break;
}
Err(err) => {
println!("{}", err);
continue;
}
}
}
Ok(())
}

View file

@ -1,28 +0,0 @@
use crossterm::style::Color;
pub mod choice;
pub mod editor;
pub(crate) fn map_color(color: Color) -> comfy_table::Color {
match color {
Color::Reset => comfy_table::Color::Reset,
Color::Black => comfy_table::Color::Black,
Color::DarkGrey => comfy_table::Color::DarkGrey,
Color::Red => comfy_table::Color::Red,
Color::DarkRed => comfy_table::Color::DarkRed,
Color::Green => comfy_table::Color::Green,
Color::DarkGreen => comfy_table::Color::DarkGreen,
Color::Yellow => comfy_table::Color::Yellow,
Color::DarkYellow => comfy_table::Color::DarkYellow,
Color::Blue => comfy_table::Color::Blue,
Color::DarkBlue => comfy_table::Color::DarkBlue,
Color::Magenta => comfy_table::Color::Magenta,
Color::DarkMagenta => comfy_table::Color::DarkMagenta,
Color::Cyan => comfy_table::Color::Cyan,
Color::DarkCyan => comfy_table::Color::DarkCyan,
Color::White => comfy_table::Color::White,
Color::Grey => comfy_table::Color::Grey,
Color::Rgb { r, g, b } => comfy_table::Color::Rgb { r, g, b },
Color::AnsiValue(n) => comfy_table::Color::AnsiValue(n),
}
}