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
- 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
# before
@ -40,10 +79,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
smtp.oauth2.method = "xoauth2"
# after
imap.auth.type = "password"
imap.auth.cmd = "pass show example"
smtp.auth.type = "oauth2"
smtp.auth.method = "xoauth2"
backend.auth.type = "password"
backend.auth.cmd = "pass show example"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.method = "xoauth2"
```
## [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.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
[#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,24 +12,22 @@ documentation = "https://github.com/pimalaya/himalaya/"
repository = "https://github.com/pimalaya/himalaya/"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--document-private-items"]
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = [
"imap",
"maildir",
# "notmuch",
#"notmuch",
"smtp",
"sendmail",
# "keyring",
# "oauth2",
#"sendmail",
#"keyring",
#"oauth2",
"wizard",
# "pgp-commands",
# "pgp-gpg",
# "pgp-native",
#"pgp-commands",
#"pgp-gpg",
#"pgp-native",
]
imap = ["email-lib/imap", "pimalaya-tui/imap"]
@ -38,57 +36,32 @@ notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib?/keyring-tokio"]
oauth2 = ["dep:oauth-lib", "email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["dep:email_address", "dep:secret-lib", "email-lib/autoconfig"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
pgp = []
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pgp"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "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", "pimalaya-tui/pgp-gpg", "pgp"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native", "pgp"]
[dependencies]
ariadne = "0.2"
async-trait = "0.1"
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6"
comfy-table = "7.1"
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 }
email-lib = { version = "=0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
mail-builder = "0.3"
md5 = "0.7"
mml-lib = { version = "=1.0.14", default-features = false, features = ["derive"] }
oauth-lib = { version = "=0.1.1", optional = true }
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
once_cell = "1.16"
petgraph = "0.6"
pimalaya-tui = { version = "=0.1.0", default-features = false, features = ["email", "path", "cli", "config", "tracing"] }
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.6", default-features = false, features = ["command", "derive"], optional = true }
pimalaya-tui = { version = "=0.1", default-features = false, features = ["email", "path", "cli", "himalaya", "tracing", "sled"] }
secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shellexpand-utils = "=0.2.1"
sled = "=0.34.7"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
tracing = "0.1"
url = "2.2"
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
[![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 Zero Entrust](https://nlnet.nl/entrust/) in 2023
- [NGI Assure](https://nlnet.nl/project/Himalaya/) in 2022
- [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:

View file

@ -288,13 +288,80 @@ template.forward.signature-style = "inlined"
template.forward.quote-headline = "-------- Forwarded Message --------\n"
########################################
#### PGP configuration #################
#### GPG-based PGP configuration #######
########################################
# TODO
#pgp.backend = "commands"
#pgp.backend = "gpg"
#pgp.backend = "native"
# Enables PGP using GPG bindings. It requires the GPG lib to be
# installed on the system, and the `pgp-gpg` cargo feature on.
#
#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 ################
@ -302,54 +369,54 @@ template.forward.quote-headline = "-------- Forwarded Message --------\n"
# Defines the IMAP backend as the default one for all features.
#
backend = "imap"
backend.type = "imap"
# IMAP server host name.
#
imap.host = "localhost"
backend.host = "localhost"
# IMAP server port.
#
#imap.port = 143
imap.port = 993
#backend.port = 143
backend.port = 993
# IMAP server encryption.
#
#imap.encryption = "none" # or false
#imap.encryption = "start-tls"
imap.encryption = "tls" # or true
#backend.encryption = "none" # or false
#backend.encryption = "start-tls"
backend.encryption = "tls" # or true
# IMAP server login.
#
imap.login = "example@localhost"
backend.login = "example@localhost"
# IMAP server password authentication configuration.
#
imap.auth.type = "password"
backend.auth.type = "password"
#
# 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
# the keyring cargo feature). You must run at least once `himalaya
# 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.
#
imap.auth.cmd = "pass show example-imap"
backend.auth.cmd = "pass show example-imap"
# IMAP server OAuth 2.0 authorization configuration.
#
#imap.auth.type = "oauth2"
#backend.auth.type = "oauth2"
#
# Client identifier issued to the client during the registration
# process described in RFC6749.
# 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
# described in RFC6749.
@ -357,23 +424,23 @@ imap.auth.cmd = "pass show example-imap"
# Defaults to keyring "<account-name>-imap-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#imap.auth.client-secret.raw = "<raw-client-secret>"
#imap.auth.client-secret.keyring = "example-imap-client-secret"
#imap.auth.client-secret.cmd = "pass show example-imap-client-secret"
#backend.auth.client-secret.raw = "<raw-client-secret>"
#backend.auth.client-secret.keyring = "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
# authentication.
#
#imap.auth.method = "oauthbearer"
#imap.auth.method = "xoauth2"
#backend.auth.method = "oauthbearer"
#backend.auth.method = "xoauth2"
#
# 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.
#
#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
# 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".
#
#imap.auth.access-token.raw = "<raw-access-token>"
#imap.auth.access-token.keyring = "example-imap-access-token"
#imap.auth.access-token.cmd = "pass show example-imap-access-token"
#backend.auth.access-token.raw = "<raw-access-token>"
#backend.auth.access-token.keyring = "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
# 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".
#
#imap.auth.refresh-token.raw = "<raw-refresh-token>"
#imap.auth.refresh-token.keyring = "example-imap-refresh-token"
#imap.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
#backend.auth.refresh-token.raw = "<raw-refresh-token>"
#backend.auth.refresh-token.keyring = "example-imap-refresh-token"
#backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
#
# Enable the protection, as defined in 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.
#
#imap.auth.scope = "unique scope"
#imap.auth.scopes = ["multiple", "scopes"]
#backend.auth.scope = "unique scope"
#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.
# Defaults to localhost.
#
#imap.auth.redirect-host = "localhost"
#backend.auth.redirect-host = "localhost"
#
# Port of the redirect server.
# Defaults to the first available one.
#
#imap.auth.redirect-port = 9999
#backend.auth.redirect-port = 9999
########################################
#### Maildir configuration #############
@ -422,18 +494,18 @@ imap.auth.cmd = "pass show example-imap"
# 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
# of the Maildir directory.
#
#maildir.root-dir = "~/.Mail/example"
#backend.root-dir = "~/.Mail/example"
# Does the Maildir folder follows the Maildir++ standard?
#
# See <https://en.wikipedia.org/wiki/Maildir#Maildir++>.
#
#maildir.maildirpp = false
#backend.maildirpp = false
########################################
#### Notmuch configuration #############
@ -441,25 +513,25 @@ imap.auth.cmd = "pass show example-imap"
# 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
# directory containing the Notmuch database (usually the root Maildir
# directory).
#
#notmuch.db-path = "~/.Mail/example"
#backend.db-path = "~/.Mail/example"
# 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.
#
#notmuch.config-path = "~/.notmuchrc"
#backend.config-path = "~/.notmuchrc"
# Override the default Notmuch profile name.
#
#notmuch.profile = "example"
#backend.profile = "example"
########################################
#### SMTP configuration ################
@ -467,55 +539,55 @@ imap.auth.cmd = "pass show example-imap"
# Defines the SMTP backend for the message sending feature.
#
message.send.backend = "smtp"
message.send.backend.type = "smtp"
# SMTP server host name.
#
smtp.host = "localhost"
message.send.backend.host = "localhost"
# SMTP server port.
#
#smtp.port = 25
#smtp.port = 465
smtp.port = 587
#message.send.backend.port = 25
#message.send.backend.port = 465
message.send.backend.port = 587
# SMTP server encryption.
#
#smtp.encryption = "none" # or false
#smtp.encryption = "start-tls"
smtp.encryption = "tls" # or true
#message.send.backend.encryption = "none" # or false
#message.send.backend.encryption = "start-tls"
message.send.backend.encryption = "tls" # or true
# SMTP server login.
#
smtp.login = "example@localhost"
message.send.backend.login = "example@localhost"
# SMTP server password authentication configuration.
#
smtp.auth.type = "password"
message.send.backend.auth.type = "password"
#
# 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
# the keyring cargo feature). You must run at least once `himalaya
# 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.
#
smtp.auth.cmd = "pass show example-smtp"
message.send.backend.auth.cmd = "pass show example-smtp"
# 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
# process described in RFC6749.
# 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
# described in RFC6749.
@ -523,23 +595,23 @@ smtp.auth.cmd = "pass show example-smtp"
# Defaults to keyring "<account-name>-smtp-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#smtp.auth.client-secret.raw = "<raw-client-secret>"
#smtp.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.raw = "<raw-client-secret>"
#message.send.backend.auth.client-secret.keyring = "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
# authentication.
#
#smtp.auth.method = "oauthbearer"
#smtp.auth.method = "xoauth2"
#message.send.backend.auth.method = "oauthbearer"
#message.send.backend.auth.method = "xoauth2"
#
# 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.
#
#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
# 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".
#
#smtp.auth.access-token.raw = "<raw-access-token>"
#smtp.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.raw = "<raw-access-token>"
#message.send.backend.auth.access-token.keyring = "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
# 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".
#
#smtp.auth.refresh-token.raw = "<raw-refresh-token>"
#smtp.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.raw = "<raw-refresh-token>"
#message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token"
#message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token"
#
# Enable the protection, as defined in 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.
#
#smtp.auth.scope = "unique scope"
#smtp.auth.scopes = ["multiple", "scopes"]
#message.send.backend.auth.scope = "unique scope"
#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.
# Defaults to localhost.
#
#smtp.auth.redirect-host = "localhost"
#message.send.backend.auth.redirect-host = "localhost"
#
# Port of the redirect server.
# Defaults to the first available one.
#
#smtp.auth.redirect-port = 9999
#message.send.backend.auth.redirect-port = 9999
########################################
#### Sendmail configuration ############
@ -588,8 +665,8 @@ smtp.auth.cmd = "pass show example-smtp"
# Defines the Sendmail backend for the message sending feature.
#
#message.send.backend = "sendmail"
#message.send.backend.type = "sendmail"
# 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 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 crate::{
account::arg::name::OptionalAccountNameArg, backend, config::Config, printer::Printer,
};
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
/// Check up the given account.
///
@ -19,102 +33,77 @@ pub struct 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");
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 used_backends = toml_account_config.get_used_backends();
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?;
let account_config = Arc::new(account_config);
match toml_account_config.backend {
#[cfg(feature = "maildir")]
{
printer.log("Checking Maildir integrity…")?;
Some(Backend::Maildir(mdir_config)) => {
printer.log("Checking Maildir integrity…\n")?;
let maildir = ctx_builder
.maildir
.as_ref()
.and_then(|maildir| maildir.check_up())
.and_then(|f| ctx.maildir.as_ref().and_then(|ctx| f(ctx)));
if let Some(maildir) = maildir.as_ref() {
maildir.check_up().await?;
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
}
#[cfg(feature = "imap")]
{
printer.log("Checking IMAP integrity…")?;
Some(Backend::Imap(imap_config)) => {
printer.log("Checking IMAP integrity…\n")?;
let imap = ctx_builder
.imap
.as_ref()
.and_then(|imap| imap.check_up())
.and_then(|f| ctx.imap.as_ref().and_then(|ctx| f(ctx)));
if let Some(imap) = imap.as_ref() {
imap.check_up().await?;
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
.with_pool_size(1);
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
}
#[cfg(feature = "notmuch")]
{
printer.log("Checking Notmuch integrity…")?;
Some(Backend::Notmuch(notmuch_config)) => {
printer.log("Checking Notmuch integrity…\n")?;
let notmuch = ctx_builder
.notmuch
.as_ref()
.and_then(|notmuch| notmuch.check_up())
.and_then(|f| ctx.notmuch.as_ref().and_then(|ctx| f(ctx)));
if let Some(notmuch) = notmuch.as_ref() {
notmuch.check_up().await?;
let ctx =
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
BackendBuilder::new(account_config.clone(), ctx)
.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")]
{
printer.log("Checking SMTP integrity…")?;
Some(SendingBackend::Smtp(smtp_config)) => {
printer.log("Checking SMTP integrity…\n")?;
let smtp = ctx_builder
.smtp
.as_ref()
.and_then(|smtp| smtp.check_up())
.and_then(|f| ctx.smtp.as_ref().and_then(|ctx| f(ctx)));
if let Some(smtp) = smtp.as_ref() {
smtp.check_up().await?;
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
}
}
#[cfg(feature = "sendmail")]
{
printer.log("Checking Sendmail integrity…")?;
Some(SendingBackend::Sendmail(sendmail_config)) => {
printer.log("Checking Sendmail integrity…\n")?;
let sendmail = ctx_builder
.sendmail
.as_ref()
.and_then(|sendmail| sendmail.check_up())
.and_then(|f| ctx.sendmail.as_ref().and_then(|ctx| f(ctx)));
if let Some(sendmail) = sendmail.as_ref() {
sendmail.check_up().await?;
let ctx =
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
BackendBuilder::new(account_config.clone(), ctx)
.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")]
use email::smtp::config::SmtpAuthConfig;
#[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;
#[cfg(any(feature = "imap", feature = "smtp"))]
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.
///
@ -31,20 +32,22 @@ pub struct 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");
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 {
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
let reset = match &config.auth {
ImapAuthConfig::Passwd(config) => config.reset().await,
{
let reset = match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => config.reset().await,
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
};
if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}");
@ -52,12 +55,14 @@ impl AccountConfigureCommand {
}
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
let reset = match &config.auth {
SmtpAuthConfig::Passwd(config) => config.reset().await,
{
let reset = match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => config.reset().await,
#[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => config.reset().await,
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await,
_ => Ok(()),
};
if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}");
@ -65,56 +70,54 @@ impl AccountConfigureCommand {
}
#[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp {
if let Some(config) = &toml_account_config.pgp {
config.reset().await?;
}
}
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
match &config.auth {
ImapAuthConfig::Passwd(config) => {
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("IMAP password")?))
.await
}
#[cfg(feature = "oauth2")]
ImapAuthConfig::OAuth2(config) => {
Some(ImapAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 clientsecret")?))
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
.await
}
_ => Ok(()),
}?;
}
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
match &config.auth {
SmtpAuthConfig::Passwd(config) => {
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("SMTP password")?))
.await
}
#[cfg(feature = "oauth2")]
SmtpAuthConfig::OAuth2(config) => {
Some(SmtpAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await
}
_ => Ok(()),
}?;
}
#[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp {
if let Some(config) = &toml_account_config.pgp {
config
.configure(&account_config.email, || {
.configure(&toml_account_config.email, || {
Ok(prompt::password("PGP secret key password")?)
})
.await?;
}
printer.out(format!(
"Account {account} successfully {}configured!",
"Account {account} successfully {}configured!\n",
if self.reset { "re" } else { "" }
))
}

View file

@ -1,12 +1,12 @@
use clap::Parser;
use color_eyre::Result;
use pimalaya_tui::{
himalaya::config::{Accounts, AccountsTable},
terminal::cli::printer::Printer,
};
use tracing::info;
use crate::{
account::{Accounts, AccountsTable},
config::Config,
printer::Printer,
};
use crate::config::TomlConfig;
/// List all accounts.
///
@ -24,7 +24,7 @@ pub struct 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");
let accounts = Accounts::from(config.accounts.iter());

View file

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

View file

@ -1,363 +1,3 @@
//! Deserialized account config module.
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
use comfy_table::presets;
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))
}
}
pub type TomlAccountConfig = HimalayaTomlAccountConfig;

View file

@ -1,198 +1,3 @@
pub mod arg;
pub mod command;
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 color_eyre::Result;
use pimalaya_tui::terminal::{
cli::{
arg::path_parser,
printer::{OutputFmt, Printer},
},
config::TomlConfig as _,
};
use std::path::PathBuf;
use crate::{
account::command::AccountSubcommand,
completion::command::CompletionGenerateCommand,
config::{self, Config},
config::TomlConfig,
envelope::command::EnvelopeSubcommand,
flag::command::FlagSubcommand,
folder::command::FolderSubcommand,
@ -14,8 +21,6 @@ use crate::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand,
},
output::OutputFmt,
printer::Printer,
};
#[derive(Parser, Debug)]
@ -34,7 +39,7 @@ pub struct Cli {
/// which allows you to separate your public config from your
/// private(s) one(s).
#[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>,
/// Customize the output format.
@ -111,31 +116,31 @@ impl HimalayaCommand {
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
match self {
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
}
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
}
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
}
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
}
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
}
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
}
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
}
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 clap::Parser;
use color_eyre::Result;
@ -5,12 +7,15 @@ use email::{
backend::feature::BackendFeatureSource, email::search_query,
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
envelope::EnvelopesTable, folder::arg::name::FolderNameOptionalFlag, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// List all envelopes.
@ -132,27 +137,32 @@ impl Default for 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let toml_account_config = Arc::new(toml_account_config);
let folder = &self.folder.name;
let page = 1.max(self.page) - 1;
let page_size = self
.page_size
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
let list_envelopes_kind = toml_account_config.list_envelopes_kind();
let backend = Backend::new(
let backend = BackendBuilder::new(
toml_account_config.clone(),
account_config.clone(),
list_envelopes_kind,
|builder| builder.set_list_envelopes(BackendFeatureSource::Context),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self

View file

@ -3,8 +3,9 @@ pub mod thread;
use clap::Subcommand;
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};
@ -25,7 +26,7 @@ pub enum EnvelopeSubcommand {
impl EnvelopeSubcommand {
#[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 {
Self::List(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,
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, envelope::EnvelopesTree,
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// Thread all envelopes.
@ -34,22 +38,27 @@ pub struct 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let account_config = Arc::new(account_config);
let folder = &self.folder.name;
let thread_envelopes_kind = toml_account_config.thread_envelopes_kind();
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
thread_envelopes_kind,
|builder| builder.set_thread_envelopes(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Add flag(s) to an envelope.
@ -29,7 +33,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagAddCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_flags_kind = toml_account_config.add_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
add_flags_kind,
|builder| builder.set_add_flags(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 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};
@ -32,7 +33,7 @@ pub enum FlagSubcommand {
impl FlagSubcommand {
#[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 {
Self::Add(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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Remove flag(s) from an envelope.
@ -29,7 +33,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagRemoveCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let remove_flags_kind = toml_account_config.remove_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
remove_flags_kind,
|builder| builder.set_remove_flags(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_remove_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Replace flag(s) of an envelope.
@ -29,7 +33,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -38,18 +42,21 @@ impl FlagSetCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let set_flags_kind = toml_account_config.set_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
set_flags_kind,
|builder| builder.set_set_flags(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_set_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 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 command;
pub mod config;
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 color_eyre::{eyre::Context, Result};
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 uuid::Uuid;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Download all attachments for the given message.
@ -28,7 +31,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -38,14 +41,19 @@ impl AttachmentDownloadCommand {
.clone()
.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(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let emails = backend.get_messages(folder, ids).await?;

View file

@ -2,8 +2,9 @@ mod download;
use clap::Subcommand;
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;
@ -19,7 +20,7 @@ pub enum 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 {
Self::Download(cmd) => cmd.execute(printer, config).await,
}

View file

@ -1,15 +1,19 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
};
/// Copy a message from a source folder to a target folder.
@ -29,7 +33,7 @@ pub struct 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");
let source = &self.source_folder.name;
@ -40,20 +44,23 @@ impl MessageCopyCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let copy_messages_kind = toml_account_config.copy_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
copy_messages_kind,
|builder| builder.set_copy_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_copy_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.copy_messages(source, target, ids).await?;
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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Mark as deleted a message from a folder.
@ -28,7 +33,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -38,18 +43,21 @@ impl MessageDeleteCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let delete_messages_kind = toml_account_config.delete_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
delete_messages_kind,
|builder| builder.set_delete_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Forward a message.
@ -39,7 +42,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -48,18 +51,20 @@ impl MessageForwardCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let id = self.envelope.id;

View file

@ -1,14 +1,17 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use mail_builder::MessageBuilder;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use url::Url;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config, printer::Printer,
ui::editor,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let mut builder = MessageBuilder::new().to(self.url.path());

View file

@ -12,8 +12,9 @@ pub mod write;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer};
use crate::config::TomlConfig;
use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
@ -67,7 +68,7 @@ pub enum MessageSubcommand {
impl MessageSubcommand {
#[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 {
Self::Read(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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
};
/// Move a message from a source folder to a target folder.
@ -30,7 +34,7 @@ pub struct 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");
let source = &self.source_folder.name;
@ -41,20 +45,23 @@ impl MessageMoveCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let move_messages_kind = toml_account_config.move_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
move_messages_kind,
|builder| builder.set_move_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_move_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.move_messages(source, target, ids).await?;
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 color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Read a message.
@ -73,7 +78,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -83,14 +88,19 @@ impl MessageReadCommand {
.clone()
.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(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let emails = if self.preview {

View file

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

View file

@ -1,13 +1,19 @@
use clap::Parser;
use color_eyre::Result;
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;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
};
/// Save a message to a folder.
@ -26,7 +32,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -35,14 +41,17 @@ impl MessageSaveCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
add_message_kind,
|builder| builder.set_add_message(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
@ -60,6 +69,6 @@ impl MessageSaveCommand {
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 color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
message::arg::MessageRawArg, printer::Printer,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
/// Send a message.
///
@ -23,28 +27,24 @@ pub struct 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config
.add_message_kind()
.filter(|_| account_config.should_save_copy_sent_message()),
);
let backend = Backend::new(
toml_account_config.clone(),
account_config,
send_message_kind,
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder.set_send_message(BackendFeatureSource::Context);
builder.set_add_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let msg = if io::stdin().is_terminal() {

View file

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

View file

@ -1,15 +1,18 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Write a new message.
@ -31,25 +34,26 @@ pub struct 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
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 attachment;
pub mod command;
pub mod config;
pub mod template;

View file

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

View file

@ -6,8 +6,9 @@ mod write;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer};
use crate::config::TomlConfig;
use self::{
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
@ -43,7 +44,7 @@ pub enum 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 {
Self::Write(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 color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
printer::Printer,
};
/// Generate a template for replying to a message.
@ -41,7 +45,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -51,14 +55,19 @@ impl TemplateReplyCommand {
.clone()
.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(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let tpl = backend

View file

@ -2,13 +2,19 @@ use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Save a template to a folder.
@ -30,7 +36,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -39,14 +45,19 @@ impl TemplateSaveCommand {
.clone()
.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(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind,
|builder| builder.set_add_message(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
@ -72,6 +83,6 @@ impl TemplateSaveCommand {
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 email::backend::feature::BackendFeatureSource;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
email::template::arg::TemplateRawArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
};
/// Send a template.
@ -26,28 +32,26 @@ pub struct 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");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config
.add_message_kind()
.filter(|_| account_config.should_save_copy_sent_message()),
);
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
send_message_kind,
|builder| {
builder.set_send_message(BackendFeatureSource::Context);
builder.set_add_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let tpl = if io::stdin().is_terminal() {

View file

@ -1,12 +1,14 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::message::Message;
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::Config,
account::arg::name::AccountNameFlag, config::TomlConfig,
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
printer::Printer,
};
/// Generate a template for writing a new message from scratch.
@ -26,14 +28,14 @@ pub struct 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");
let (_, account_config) = config
.clone()
.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_body(self.body.raw())
.build()

View file

@ -1,11 +1,16 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Create a new folder.
@ -22,26 +27,30 @@ pub struct 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");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref())?;
let add_folder_kind = toml_account_config.add_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
add_folder_kind,
|builder| builder.set_add_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Delete a folder.
@ -25,12 +27,13 @@ pub struct 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");
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)? {
process::exit(0);
@ -40,18 +43,21 @@ impl FolderDeleteCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let delete_folder_kind = toml_account_config.delete_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
delete_folder_kind,
|builder| builder.set_delete_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Expunge a folder.
@ -23,7 +28,7 @@ pub struct 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");
let folder = &self.folder.name;
@ -31,18 +36,21 @@ impl FolderExpungeCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let expunge_folder_kind = toml_account_config.expunge_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
expunge_folder_kind,
|builder| builder.set_expunge_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_expunge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::Config,
folder::{Folders, FoldersTable},
printer::Printer,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// List all folders.
///
@ -29,21 +32,26 @@ pub struct 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");
let (toml_account_config, account_config) = config
.clone()
.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(),
account_config.clone(),
list_folders_kind,
|builder| builder.set_list_folders(BackendFeatureSource::Context),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_folders(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let folders = Folders::from(backend.list_folders().await?);

View file

@ -6,8 +6,9 @@ mod purge;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::Config, printer::Printer};
use crate::config::TomlConfig;
use self::{
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
@ -38,7 +39,7 @@ pub enum FolderSubcommand {
impl FolderSubcommand {
#[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 {
Self::Add(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 color_eyre::Result;
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 crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::Config,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Purge a folder.
@ -25,12 +27,13 @@ pub struct 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");
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)? {
process::exit(0);
@ -40,18 +43,21 @@ impl FolderPurgeCommand {
.clone()
.into_account_configs(self.account.name.as_deref())?;
let purge_folder_kind = toml_account_config.purge_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
purge_folder_kind,
|builder| builder.set_purge_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_purge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.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 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 backend;
pub mod cache;
pub mod cli;
pub mod completion;
pub mod config;
pub mod email;
pub mod folder;
pub mod manual;
pub mod output;
pub mod printer;
pub mod ui;
#[doc(inline)]
pub use crate::email::{envelope, flag, message};

View file

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

View file

@ -1,11 +1,12 @@
use clap::{CommandFactory, Parser};
use clap_mangen::Man;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use shellexpand_utils::{canonicalize, expand};
use std::{fs, path::PathBuf};
use tracing::info;
use crate::{cli::Cli, printer::Printer};
use crate::cli::Cli;
/// 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),
}
}