├── .github ├── FUNDING.yml └── workflows │ ├── release-on-demand.yml │ └── releases.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── assets └── himalaya.desktop ├── build.rs ├── config.sample.toml ├── default.nix ├── flake.lock ├── flake.nix ├── install.sh ├── logo-small.svg ├── logo.svg ├── package.nix ├── rust-toolchain.toml ├── screenshot.jpeg ├── shell.nix └── src ├── account ├── arg │ ├── mod.rs │ └── name.rs ├── command │ ├── configure.rs │ ├── doctor.rs │ ├── list.rs │ └── mod.rs ├── config.rs └── mod.rs ├── cli.rs ├── completion ├── command.rs └── mod.rs ├── config.rs ├── email ├── envelope │ ├── arg │ │ ├── ids.rs │ │ └── mod.rs │ ├── command │ │ ├── list.rs │ │ ├── mod.rs │ │ └── thread.rs │ ├── flag │ │ ├── arg │ │ │ ├── ids_and_flags.rs │ │ │ └── mod.rs │ │ ├── command │ │ │ ├── add.rs │ │ │ ├── mod.rs │ │ │ ├── remove.rs │ │ │ └── set.rs │ │ └── mod.rs │ └── mod.rs ├── message │ ├── arg │ │ ├── body.rs │ │ ├── header.rs │ │ ├── mod.rs │ │ └── reply.rs │ ├── attachment │ │ ├── command │ │ │ ├── download.rs │ │ │ └── mod.rs │ │ └── mod.rs │ ├── command │ │ ├── copy.rs │ │ ├── delete.rs │ │ ├── edit.rs │ │ ├── export.rs │ │ ├── forward.rs │ │ ├── mailto.rs │ │ ├── mod.rs │ │ ├── move.rs │ │ ├── read.rs │ │ ├── reply.rs │ │ ├── save.rs │ │ ├── send.rs │ │ ├── thread.rs │ │ └── write.rs │ ├── mod.rs │ └── template │ │ ├── arg │ │ ├── body.rs │ │ └── mod.rs │ │ ├── command │ │ ├── forward.rs │ │ ├── mod.rs │ │ ├── reply.rs │ │ ├── save.rs │ │ ├── send.rs │ │ └── write.rs │ │ └── mod.rs └── mod.rs ├── folder ├── arg │ ├── mod.rs │ └── name.rs ├── command │ ├── add.rs │ ├── delete.rs │ ├── expunge.rs │ ├── list.rs │ ├── mod.rs │ └── purge.rs └── mod.rs ├── lib.rs ├── main.rs └── manual ├── command.rs └── mod.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: soywod 2 | ko_fi: soywod 3 | buy_me_a_coffee: soywod 4 | liberapay: soywod 5 | thanks_dev: soywod 6 | custom: https://www.paypal.com/paypalme/soywod 7 | -------------------------------------------------------------------------------- /.github/workflows/release-on-demand.yml: -------------------------------------------------------------------------------- 1 | name: Release on demand 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | os: 7 | description: Operating system 8 | type: choice 9 | required: true 10 | default: ubuntu-latest 11 | options: 12 | - ubuntu-24.04 13 | - macos-13 14 | - macos-14 15 | target: 16 | description: Architecture 17 | type: choice 18 | required: true 19 | options: 20 | - aarch64-apple-darwin 21 | - aarch64-unknown-linux-musl 22 | - aarch64-unknown-linux-musl 23 | - armv6l-unknown-linux-musleabihf 24 | - armv7l-unknown-linux-musleabihf 25 | - i686-unknown-linux-musl 26 | - x86_64-apple-darwin 27 | - x86_64-unknown-linux-musl 28 | - x86_64-w64-mingw32 29 | features: 30 | description: Cargo features 31 | type: string 32 | required: true 33 | 34 | jobs: 35 | release-on-demand: 36 | uses: pimalaya/nix/.github/workflows/release-on-demand.yml@master 37 | secrets: inherit 38 | with: 39 | project: himalaya 40 | os: ${{ inputs.os }} 41 | target: ${{ inputs.target }} 42 | features: ${{ inputs.features }} 43 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | name: Releases 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | branches: 8 | - master 9 | 10 | jobs: 11 | release: 12 | uses: pimalaya/nix/.github/workflows/releases.yml@master 13 | secrets: inherit 14 | with: 15 | project: himalaya 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Cargo config directory 2 | .cargo/ 3 | 4 | # Cargo build directory 5 | target/ 6 | debug/ 7 | 8 | # These are backup files generated by rustfmt 9 | **/*.rs.bk 10 | 11 | # MSVC Windows builds of rustc generate these, which store debugging information 12 | *.pdb 13 | 14 | # Nix build directory 15 | result 16 | result-* 17 | 18 | # Direnv 19 | /.envrc 20 | /.direnv 21 | 22 | 23 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 24 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 25 | .idea/ 26 | 27 | # IntelliJ 28 | out/ 29 | 30 | # mpeltonen/sbt-idea plugin 31 | .idea_modules/ 32 | 33 | # JIRA plugin 34 | atlassian-ide-plugin.xml 35 | 36 | # Cursive Clojure plugin 37 | .idea/replstate.xml 38 | 39 | # SonarLint plugin 40 | .idea/sonarlint/ 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | 48 | ## Others 49 | .metadata/ 50 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "matklad.rust-analyzer", 4 | "arrterian.nix-env-selector" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix" 3 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | Thank you for investing your time in contributing to Himalaya CLI! 4 | 5 | ## Development 6 | 7 | The development environment is managed by [Nix](https://nixos.org/download.html). 8 | Running `nix-shell` will spawn a shell with everything you need to get started with the lib. 9 | 10 | If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html): 11 | 12 | ```text 13 | rustup update 14 | ``` 15 | 16 | or install manually the following dependencies: 17 | 18 | - [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`) 19 | - [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`) 20 | 21 | ## Build 22 | 23 | ```text 24 | cargo build 25 | ``` 26 | 27 | You can disable default [features](https://doc.rust-lang.org/cargo/reference/features.html) with `--no-default-features` and enable features with `--features feat1,feat2,feat3`. 28 | 29 | Finally, you can build a release with `--release`: 30 | 31 | ```text 32 | cargo build --no-default-features --features imap,smtp,keyring --release 33 | ``` 34 | 35 | ## Override dependencies 36 | 37 | If you want to build Himalaya CLI with a dependency installed locally (for example `email-lib`), then you can [override it](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html) by adding the following lines at the end of `Cargo.toml`: 38 | 39 | ```toml 40 | [patch.crates-io] 41 | email-lib = { path = "/path/to/email-lib" } 42 | ``` 43 | 44 | If you get the following error: 45 | 46 | ```text 47 | note: perhaps two different versions of crate email are being used? 48 | ``` 49 | 50 | then you may need to override more Pimalaya's sub-dependencies: 51 | 52 | ```toml 53 | [patch.crates-io] 54 | email-lib.path = "/path/to/core/email" 55 | imap-client.path = "/path/to/imap-client" 56 | keyring-lib.path = "/path/to/core/keyring" 57 | mml-lib.path = "/path/to/core/mml" 58 | oauth-lib.path = "/path/to/core/oauth" 59 | pgp-lib.path = "/path/to/core/pgp" 60 | pimalaya-tui.path = "/path/to/tui" 61 | process-lib.path = "/path/to/core/process" 62 | secret-lib.path = "/path/to/core/secret" 63 | ``` 64 | 65 | *See [pimalaya/core#32](https://github.com/pimalaya/core/issues/32) for more information.* 66 | 67 | ## Commit style 68 | 69 | Starting from the `v1.0.0`, Himalaya CLI tries to adopt the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary). 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "himalaya" 3 | description = "CLI to manage emails" 4 | version = "1.1.0" 5 | authors = ["soywod "] 6 | edition = "2021" 7 | license = "MIT" 8 | categories = ["command-line-utilities", "email"] 9 | keywords = ["cli", "email", "imap", "maildir", "smtp"] 10 | homepage = "https://pimalaya.org/" 11 | documentation = "https://github.com/pimalaya/himalaya/" 12 | repository = "https://github.com/pimalaya/himalaya/" 13 | 14 | [package.metadata.docs.rs] 15 | features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"] 16 | rustdoc-args = ["--cfg", "docsrs"] 17 | 18 | [features] 19 | default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"] 20 | imap = ["email-lib/imap", "pimalaya-tui/imap"] 21 | maildir = ["email-lib/maildir", "pimalaya-tui/maildir"] 22 | notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"] 23 | smtp = ["email-lib/smtp", "pimalaya-tui/smtp"] 24 | sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"] 25 | keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"] 26 | oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"] 27 | wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"] 28 | pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"] 29 | pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"] 30 | pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"] 31 | 32 | [build-dependencies] 33 | pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] } 34 | 35 | [dev-dependencies] 36 | himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] } 37 | 38 | [dependencies] 39 | ariadne = "0.2" 40 | clap = { version = "4.4", features = ["derive", "env", "wrap_help"] } 41 | clap_complete = "4.4" 42 | clap_mangen = "0.2" 43 | color-eyre = "0.6" 44 | email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] } 45 | mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] } 46 | once_cell = "1.16" 47 | open = "5.3" 48 | pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] } 49 | secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] } 50 | serde = { version = "1", features = ["derive"] } 51 | serde_json = "1" 52 | shellexpand-utils = "=0.2.1" 53 | tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] } 54 | toml = "0.8" 55 | tracing = "0.1" 56 | url = "2.2" 57 | uuid = { version = "0.8", features = ["v4"] } 58 | 59 | [patch.crates-io] 60 | imap-codec.git = "https://github.com/duesee/imap-codec" 61 | 62 | email-lib.git = "https://github.com/pimalaya/core" 63 | imap-client.git = "https://github.com/pimalaya/imap-client" 64 | keyring-lib.git = "https://github.com/pimalaya/core" 65 | mml-lib.git = "https://github.com/pimalaya/core" 66 | oauth-lib.git = "https://github.com/pimalaya/core" 67 | pgp-lib.git = "https://github.com/pimalaya/core" 68 | pimalaya-tui.git = "https://github.com/pimalaya/tui" 69 | process-lib.git = "https://github.com/pimalaya/core" 70 | secret-lib.git = "https://github.com/pimalaya/core" 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2024 soywod 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/himalaya.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=himalaya 4 | DesktopName=Himalaya 5 | GenericName=Mail Reader 6 | Comment=CLI to manage emails 7 | Terminal=true 8 | Exec=himalaya %U 9 | Categories=Application;Network 10 | Keywords=email 11 | MimeType=x-scheme-handler/mailto;message/rfc822 12 | Actions=Compose 13 | 14 | [Desktop Action Compose] 15 | Name=Compose 16 | Exec=himalaya message write %U 17 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use pimalaya_tui::build::{features_env, git_envs, target_envs}; 2 | 3 | fn main() { 4 | features_env(include_str!("./Cargo.toml")); 5 | target_envs(); 6 | git_envs(); 7 | } 8 | -------------------------------------------------------------------------------- /config.sample.toml: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | ###[ Global configuration ]##################################################### 3 | ################################################################################ 4 | 5 | # Default display name for all accounts. It is used to build the full 6 | # email address of an account: "Example" 7 | # 8 | display-name = "Example" 9 | 10 | # Default signature for all accounts. The signature is put at the 11 | # bottom of all messages. It can be a path or a string. Supports TOML 12 | # multilines. 13 | # 14 | #signature = "/path/to/signature/file" 15 | #signature = """ 16 | # Thanks you, 17 | # Regards 18 | #""" 19 | signature = "Regards,\n" 20 | 21 | # Default signature delimiter for all accounts. It delimits the end of 22 | # the message body from the signature. 23 | # 24 | signature-delim = "-- \n" 25 | 26 | # Default downloads directory path for all accounts. It is mostly used 27 | # for downloading attachments. Defaults to the system temporary 28 | # directory. 29 | # 30 | downloads-dir = "~/Downloads" 31 | 32 | # Customizes the charset used to build the accounts listing 33 | # table. Defaults to markdown table style. 34 | # 35 | # See . 36 | # 37 | account.list.table.preset = "|| |-||| " 38 | 39 | # Customizes the color of the NAME column of the account listing 40 | # table. 41 | # 42 | account.list.table.name-color = "green" 43 | 44 | # Customizes the color of the BACKENDS column of the account listing 45 | # table. 46 | # 47 | account.list.table.backends-color = "blue" 48 | 49 | # Customizes the color of the DEFAULT column of the account listing 50 | # table. 51 | # 52 | account.list.table.default-color = "black" 53 | 54 | ################################################################################ 55 | ###[ Account configuration ]#################################################### 56 | ################################################################################ 57 | 58 | # The account name should be unique. 59 | # 60 | [accounts.example] 61 | 62 | # Defaultness of the account. The current account will be used by 63 | # default in all commands. 64 | # 65 | default = true 66 | 67 | # The email address associated to the current account. 68 | # 69 | email = "example@localhost" 70 | 71 | # The display name of the account. This and the email are used to 72 | # build the full email address: "Example" 73 | # 74 | display-name = "Example" 75 | 76 | # The signature put at the bottom of composed messages. It can be a 77 | # path or a string. Supports TOML multilines. 78 | # 79 | #signature = "/path/to/signature/file" 80 | #signature = """ 81 | # Thanks you, 82 | # Regards 83 | #""" 84 | signature = "Regards,\n" 85 | 86 | # Signature delimiter. It delimits the end of the message body from 87 | # the signature. 88 | # 89 | signature-delim = "-- \n" 90 | 91 | # Downloads directory path. It is mostly used for downloading 92 | # attachments. Defaults to the system temporary directory. 93 | # 94 | downloads-dir = "~/downloads" 95 | 96 | 97 | 98 | # Defines aliases for your mailboxes. There are 4 special aliases used 99 | # by the tool: inbox, sent, drafts and trash. Other aliases can be 100 | # defined as well. 101 | # 102 | folder.aliases.inbox = "INBOX" 103 | folder.aliases.sent = "Sent" 104 | folder.aliases.drafts = "Drafts" 105 | folder.aliases.trash = "Trash" 106 | folder.aliases.a23 = "Archives/2023" 107 | 108 | # Customizes the number of folders to show by page. 109 | # 110 | folder.list.page-size = 10 111 | 112 | # Customizes the charset used to build the table. Defaults to markdown 113 | # table style. 114 | # 115 | # See . 116 | # 117 | folder.list.table.preset = "|| |-||| " 118 | 119 | # Customizes the color of the NAME column of the folder listing table. 120 | # 121 | folder.list.table.name-color = "blue" 122 | 123 | # Customizes the color of the DESC column of the folder listing table. 124 | # 125 | folder.list.table.desc-color = "green" 126 | 127 | 128 | 129 | # Customizes the number of envelopes to show by page. 130 | # 131 | envelope.list.page-size = 10 132 | 133 | # Customizes the format of the envelope date. 134 | # 135 | # See supported formats at . 136 | # 137 | envelope.list.datetime-fmt = "%F %R%:z" 138 | 139 | # Transforms envelopes date timezone into the user's local one. For 140 | # example, if the user's local timezone is UTC, the envelope date 141 | # `2023-06-15T09:00:00+02:00` becomes `2023-06-15T07:00:00-00:00`. 142 | # 143 | envelope.list.datetime-local-tz = true 144 | 145 | # Customizes the charset used to build the table. Defaults to markdown 146 | # table style. 147 | # 148 | # See . 149 | # 150 | envelope.list.table.preset = "|| |-||| " 151 | 152 | # Customizes the character of the unseen flag of the envelope listing 153 | # table. 154 | # 155 | envelope.list.table.unseen-char = "*" 156 | 157 | # Customizes the character of the replied flag of the envelope listing 158 | # table. 159 | # 160 | envelope.list.table.replied-char = "R" 161 | 162 | # Customizes the character of the flagged flag of the envelope listing 163 | # table. 164 | # 165 | envelope.list.table.flagged-char = "!" 166 | 167 | # Customizes the character of the attachment property of the envelope 168 | # listing table. 169 | # 170 | envelope.list.table.attachment-char = "@" 171 | 172 | # Customizes the color of the ID column of the envelope listing table. 173 | # 174 | envelope.list.table.id-color = "red" 175 | 176 | # Customizes the color of the FLAGS column of the envelope listing 177 | # table. 178 | # 179 | envelope.list.table.flags-color = "black" 180 | 181 | # Customizes the color of the SUBJECT column of the envelope listing 182 | # table. 183 | # 184 | envelope.list.table.subject-color = "green" 185 | 186 | # Customizes the color of the SENDER column of the envelope listing 187 | # table. 188 | # 189 | envelope.list.table.sender-color = "blue" 190 | 191 | # Customizes the color of the DATE column of the envelope listing 192 | # table. 193 | # 194 | envelope.list.table.date-color = "yellow" 195 | 196 | 197 | 198 | # Defines headers to show at the top of messages when reading them. 199 | # 200 | message.read.headers = ["From", "To", "Cc", "Subject"] 201 | 202 | # Represents the message text/plain format as defined in the 203 | # RFC2646. 204 | # 205 | # See . 206 | # 207 | #message.read.format.fixed = 80 208 | #message.read.format = "flowed" 209 | message.read.format = "auto" 210 | 211 | # Defines headers to show at the top of messages when writing them. 212 | # 213 | message.write.headers = ["From", "To", "In-Reply-To", "Cc", "Subject"] 214 | 215 | # Saves a copy of sent messages to the sent folder. The sent folder is 216 | # taken from folder.aliases, defaults to Sent. 217 | # 218 | message.send.save-copy = true 219 | 220 | # Hook called just before sending a message. The command should take a 221 | # raw message as standard input (stdin) and returns the modified raw 222 | # message to the standard output (stdout). 223 | # 224 | message.send.pre-hook = "process-markdown.sh" 225 | 226 | # Customizes the message deletion style. Message deletion can be 227 | # performed either by moving messages to the Trash folder or by adding 228 | # the Deleted flag to their respective envelopes. 229 | # 230 | #message.delete.style = "flag" 231 | message.delete.style = "folder" 232 | 233 | 234 | 235 | # Defines how and where the signature should be displayed when writing 236 | # a new message. 237 | # 238 | #template.new.signature-style = "hidden" 239 | #template.new.signature-style = "attached" 240 | template.new.signature-style = "inlined" 241 | 242 | # Defines the posting style when replying to a message. 243 | # 244 | # See . 245 | # 246 | #template.reply.posting-style = "interleaved" 247 | #template.reply.posting-style = "bottom" 248 | template.reply.posting-style = "top" 249 | 250 | # Defines how and where the signature should be displayed when 251 | # repyling to a message. 252 | # 253 | #template.reply.signature-style = "hidden" 254 | #template.reply.signature-style = "attached" 255 | #template.reply.signature-style = "above-quote" 256 | template.reply.signature-style = "below-quote" 257 | 258 | # Defines the headline format put at the top of a quote when replying 259 | # to a message. 260 | # 261 | # Available placeholders: {senders} 262 | # See supported date formats at . 263 | # 264 | template.reply.quote-headline-fmt = "On %d/%m/%Y %H:%M, {senders} wrote:\n" 265 | 266 | # Defines the posting style when forwarding a message. 267 | # 268 | # See . 269 | # 270 | #template.forward.posting-style = "attached" 271 | template.forward.posting-style = "top" 272 | 273 | # Defines how and where the signature should be displayed when 274 | # forwarding a message. 275 | # 276 | #template.forward.signature-style = "hidden" 277 | #template.forward.signature-style = "attached" 278 | template.forward.signature-style = "inlined" 279 | 280 | # Defines the headline format put at the top of the quote when 281 | # forwarding a message. 282 | # 283 | template.forward.quote-headline = "-------- Forwarded Message --------\n" 284 | 285 | 286 | 287 | # Enables PGP using GPG bindings. It requires the GPG lib to be 288 | # installed on the system, and the `pgp-gpg` cargo feature on. 289 | # 290 | #pgp.type = "gpg" 291 | 292 | 293 | 294 | # Enables PGP using shell commands. A PGP client needs to be installed 295 | # on the system, like gpg. It also requires the `pgp-commands` cargo 296 | # feature. 297 | # 298 | #pgp.type = "commands" 299 | 300 | # Defines the encrypt command. The special placeholder `` 301 | # represents the list of recipients, formatted by 302 | # `pgp.encrypt-recipient-fmt`. 303 | # 304 | #pgp.encrypt-cmd = "gpg --encrypt --quiet --armor " 305 | 306 | # Formats recipients for `pgp.encrypt-cmd`. The special placeholder 307 | # `` is replaced by an actual recipient at runtime. 308 | # 309 | #pgp.encrypt-recipient-fmt = "--recipient " 310 | 311 | # Defines the separator used between formatted recipients 312 | # `pgp.encrypt-recipient-fmt`. 313 | # 314 | #pgp.encrypt-recipients-sep = " " 315 | 316 | # Defines the decrypt command. 317 | # 318 | #pgp.decrypt-cmd = "gpg --decrypt --quiet" 319 | 320 | # Defines the sign command. 321 | # 322 | #pgp.sign-cmd = "gpg --sign --quiet --armor" 323 | 324 | # Defines the verify command. 325 | # 326 | #pgp.verify-cmd = "gpg --verify --quiet" 327 | 328 | 329 | 330 | # Enables the native Rust implementation of PGP. It requires the 331 | # `pgp-native` cargo feature. 332 | # 333 | #pgp.type = "native" 334 | 335 | # Defines where to find the PGP secret key. 336 | # 337 | #pgp.secret-key.path = "/path/to/secret.key" 338 | #pgp.secret-key.keyring = "my-pgp-secret-key" 339 | 340 | # Defines how to retrieve the PGP secret key passphrase. 341 | # 342 | #pgp.secret-key-passphrase.raw = "p@assw0rd" 343 | #pgp.secret-key-passphrase.keyring = "my-pgp-passphrase" 344 | #pgp.secret-key-passphrase.cmd = "pass show pgp-passphrase" 345 | 346 | # Enables the Web Key Discovery protocol to discover recipients' 347 | # public key based on their email address. 348 | # 349 | #pgp.wkd = true 350 | 351 | # Enables public key servers discovery. 352 | # 353 | #pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"] 354 | 355 | 356 | 357 | # Defines the IMAP backend as the default one for all features. 358 | # 359 | backend.type = "imap" 360 | 361 | # IMAP server host name. 362 | # 363 | backend.host = "localhost" 364 | 365 | # IMAP server port. 366 | # 367 | #backend.port = 143 368 | backend.port = 993 369 | 370 | # IMAP server encryption. 371 | # 372 | #backend.encryption.type = "none" 373 | #backend.encryption.type = "start-tls" 374 | backend.encryption.type = "tls" 375 | 376 | # IMAP server login. 377 | # 378 | backend.login = "example@localhost" 379 | 380 | # IMAP server password authentication configuration. 381 | # 382 | backend.auth.type = "password" 383 | # 384 | # Password can be inlined (not recommended). 385 | # 386 | #backend.auth.raw = "p@assw0rd" 387 | # 388 | # Password can be stored inside your system global keyring (requires 389 | # the keyring cargo feature). You must run at least once `himalaya 390 | # account configure` to set up the password. 391 | # 392 | #backend.auth.keyring = "example-imap" 393 | # 394 | # Password can be retrieved from a shell command. 395 | # 396 | backend.auth.cmd = "pass show example-imap" 397 | 398 | # IMAP server OAuth 2.0 authorization configuration. 399 | # 400 | #backend.auth.type = "oauth2" 401 | # 402 | # Client identifier issued to the client during the registration 403 | # process described in RFC6749. 404 | # See . 405 | # 406 | #backend.auth.client-id = "client-id" 407 | # 408 | # Client password issued to the client during the registration process 409 | # described in RFC6749. 410 | # 411 | # Defaults to keyring "-imap-client-secret". 412 | # See . 413 | # 414 | #backend.auth.client-secret.raw = "" 415 | #backend.auth.client-secret.keyring = "example-imap-client-secret" 416 | #backend.auth.client-secret.cmd = "pass show example-imap-client-secret" 417 | # 418 | # Method for presenting an OAuth 2.0 bearer token to a service for 419 | # authentication. 420 | # 421 | #backend.auth.method = "oauthbearer" 422 | #backend.auth.method = "xoauth2" 423 | # 424 | # URL of the authorization server's authorization endpoint. 425 | # 426 | #backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth" 427 | # 428 | # URL of the authorization server's token endpoint. 429 | # 430 | #backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token" 431 | # 432 | # Access token returned by the token endpoint and used to access 433 | # protected resources. It is recommended to use the keyring variant, 434 | # as it will refresh automatically. 435 | # 436 | # Defaults to keyring "-imap-access-token". 437 | # 438 | #backend.auth.access-token.raw = "" 439 | #backend.auth.access-token.keyring = "example-imap-access-token" 440 | #backend.auth.access-token.cmd = "pass show example-imap-access-token" 441 | # 442 | # Refresh token used to obtain a new access token (if supported by the 443 | # authorization server). It is recommended to use the keyring variant, 444 | # as it will refresh automatically. 445 | # 446 | # Defaults to keyring "-imap-refresh-token". 447 | # 448 | #backend.auth.refresh-token.raw = "" 449 | #backend.auth.refresh-token.keyring = "example-imap-refresh-token" 450 | #backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token" 451 | # 452 | # Enable the protection, as defined in RFC7636. 453 | # 454 | # See . 455 | # 456 | #backend.auth.pkce = true 457 | # 458 | # Access token scope(s), as defined by the authorization server. 459 | # 460 | #backend.auth.scope = "unique scope" 461 | #backend.auth.scopes = ["multiple", "scopes"] 462 | # 463 | # URL scheme of the redirect server. 464 | # Defaults to http. 465 | # 466 | #backend.auth.redirect-scheme = "http" 467 | # 468 | # Host name of the redirect server. 469 | # Defaults to localhost. 470 | # 471 | #backend.auth.redirect-host = "localhost" 472 | # 473 | # Port of the redirect server. 474 | # Defaults to the first available one. 475 | # 476 | #backend.auth.redirect-port = 9999 477 | 478 | 479 | 480 | # Defines the Maildir backend as the default one for all features. 481 | # 482 | #backend.type = "maildir" 483 | 484 | # The Maildir root directory. The path should point to the root level 485 | # of the Maildir directory. 486 | # 487 | #backend.root-dir = "~/.Mail/example" 488 | 489 | # Does the Maildir folder follows the Maildir++ standard? 490 | # 491 | # See . 492 | # 493 | #backend.maildirpp = false 494 | 495 | 496 | 497 | # Defines the Notmuch backend as the default one for all features. 498 | # 499 | #backend.type = "notmuch" 500 | 501 | # The path to the Notmuch database. The path should point to the root 502 | # directory containing the Notmuch database (usually the root Maildir 503 | # directory). 504 | # 505 | #backend.db-path = "~/.Mail/example" 506 | 507 | # Overrides the default path to the Maildir folder. 508 | # 509 | #backend.maildir-path = "~/.Mail/example" 510 | 511 | # Overrides the default Notmuch configuration file path. 512 | # 513 | #backend.config-path = "~/.notmuchrc" 514 | 515 | # Override the default Notmuch profile name. 516 | # 517 | #backend.profile = "example" 518 | 519 | 520 | 521 | # Defines the SMTP backend for the message sending feature. 522 | # 523 | message.send.backend.type = "smtp" 524 | 525 | # SMTP server host name. 526 | # 527 | message.send.backend.host = "localhost" 528 | 529 | # SMTP server port. 530 | # 531 | #message.send.backend.port = 25 532 | #message.send.backend.port = 465 533 | message.send.backend.port = 587 534 | 535 | # SMTP server encryption. 536 | # 537 | #message.send.backend.encryption.type = "none" 538 | #message.send.backend.encryption.type = "start-tls" 539 | message.send.backend.encryption.type = "tls" 540 | 541 | # SMTP server login. 542 | # 543 | message.send.backend.login = "example@localhost" 544 | 545 | # SMTP server password authentication configuration. 546 | # 547 | message.send.backend.auth.type = "password" 548 | # 549 | # Password can be inlined (not recommended). 550 | # 551 | #message.send.backend.auth.raw = "p@assw0rd" 552 | # 553 | # Password can be stored inside your system global keyring (requires 554 | # the keyring cargo feature). You must run at least once `himalaya 555 | # account configure` to set up the password. 556 | # 557 | #message.send.backend.auth.keyring = "example-smtp" 558 | # 559 | # Password can be retrieved from a shell command. 560 | # 561 | message.send.backend.auth.cmd = "pass show example-smtp" 562 | 563 | # SMTP server OAuth 2.0 authorization configuration. 564 | # 565 | #message.send.backend.auth.type = "oauth2" 566 | # 567 | # Client identifier issued to the client during the registration 568 | # process described in RFC6749. 569 | # See . 570 | # 571 | #message.send.backend.auth.client-id = "client-id" 572 | # 573 | # Client password issued to the client during the registration process 574 | # described in RFC6749. 575 | # 576 | # Defaults to keyring "-smtp-client-secret". 577 | # See . 578 | # 579 | #message.send.backend.auth.client-secret.raw = "" 580 | #message.send.backend.auth.client-secret.keyring = "example-smtp-client-secret" 581 | #message.send.backend.auth.client-secret.cmd = "pass show example-smtp-client-secret" 582 | # 583 | # Method for presenting an OAuth 2.0 bearer token to a service for 584 | # authentication. 585 | # 586 | #message.send.backend.auth.method = "oauthbearer" 587 | #message.send.backend.auth.method = "xoauth2" 588 | # 589 | # URL of the authorization server's authorization endpoint. 590 | # 591 | #message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth" 592 | # 593 | # URL of the authorization server's token endpoint. 594 | # 595 | #message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token" 596 | # 597 | # Access token returned by the token endpoint and used to access 598 | # protected resources. It is recommended to use the keyring variant, 599 | # as it will refresh automatically. 600 | # 601 | # Defaults to keyring "-smtp-access-token". 602 | # 603 | #message.send.backend.auth.access-token.raw = "" 604 | #message.send.backend.auth.access-token.keyring = "example-smtp-access-token" 605 | #message.send.backend.auth.access-token.cmd = "pass show example-smtp-access-token" 606 | # 607 | # Refresh token used to obtain a new access token (if supported by the 608 | # authorization server). It is recommended to use the keyring variant, 609 | # as it will refresh automatically. 610 | # 611 | # Defaults to keyring "-smtp-refresh-token". 612 | # 613 | #message.send.backend.auth.refresh-token.raw = "" 614 | #message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token" 615 | #message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token" 616 | # 617 | # Enable the protection, as defined in RFC7636. 618 | # 619 | # See . 620 | # 621 | #message.send.backend.auth.pkce = true 622 | # 623 | # Access token scope(s), as defined by the authorization server. 624 | # 625 | #message.send.backend.auth.scope = "unique scope" 626 | #message.send.backend.auth.scopes = ["multiple", "scopes"] 627 | # 628 | # URL scheme of the redirect server. 629 | # Defaults to http. 630 | # 631 | #message.send.backend.auth.redirect-scheme = "http" 632 | # 633 | # Host name of the redirect server. 634 | # Defaults to localhost. 635 | # 636 | #message.send.backend.auth.redirect-host = "localhost" 637 | # 638 | # Port of the redirect server. 639 | # Defaults to the first available one. 640 | # 641 | #message.send.backend.auth.redirect-port = 9999 642 | 643 | 644 | 645 | # Defines the Sendmail backend for the message sending feature. 646 | # 647 | #message.send.backend.type = "sendmail" 648 | 649 | # Customizes the sendmail shell command. 650 | # 651 | #message.send.backend.cmd = "/usr/bin/sendmail" 652 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"), 3 | ... 4 | }@args: 5 | 6 | pimalaya.mkDefault ( 7 | { 8 | src = ./.; 9 | version = "1.0.0"; 10 | mkPackage = ( 11 | { 12 | lib, 13 | pkgs, 14 | rustPlatform, 15 | defaultFeatures, 16 | features, 17 | }: 18 | pkgs.callPackage ./package.nix { 19 | inherit lib rustPlatform; 20 | apple-sdk = pkgs.apple-sdk; 21 | installShellCompletions = false; 22 | installManPages = false; 23 | buildNoDefaultFeatures = !defaultFeatures; 24 | buildFeatures = lib.splitString "," features; 25 | } 26 | ); 27 | } 28 | // removeAttrs args [ "pimalaya" ] 29 | ) 30 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "fenix": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "rust-analyzer-src": "rust-analyzer-src" 9 | }, 10 | "locked": { 11 | "lastModified": 1732405626, 12 | "narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=", 13 | "owner": "soywod", 14 | "repo": "fenix", 15 | "rev": "c7af381484169a78fb79a11652321ae80b0f92a6", 16 | "type": "github" 17 | }, 18 | "original": { 19 | "owner": "soywod", 20 | "repo": "fenix", 21 | "type": "github" 22 | } 23 | }, 24 | "nixpkgs": { 25 | "locked": { 26 | "lastModified": 1736437047, 27 | "narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=", 28 | "owner": "nixos", 29 | "repo": "nixpkgs", 30 | "rev": "f17b95775191ea44bc426831235d87affb10faba", 31 | "type": "github" 32 | }, 33 | "original": { 34 | "owner": "nixos", 35 | "ref": "staging-next", 36 | "repo": "nixpkgs", 37 | "type": "github" 38 | } 39 | }, 40 | "pimalaya": { 41 | "flake": false, 42 | "locked": { 43 | "lastModified": 1737984647, 44 | "narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=", 45 | "owner": "pimalaya", 46 | "repo": "nix", 47 | "rev": "712a481632f4929d24a34cb5762e0ffdc901bd99", 48 | "type": "github" 49 | }, 50 | "original": { 51 | "owner": "pimalaya", 52 | "repo": "nix", 53 | "type": "github" 54 | } 55 | }, 56 | "root": { 57 | "inputs": { 58 | "fenix": "fenix", 59 | "nixpkgs": "nixpkgs", 60 | "pimalaya": "pimalaya" 61 | } 62 | }, 63 | "rust-analyzer-src": { 64 | "flake": false, 65 | "locked": { 66 | "lastModified": 1732050317, 67 | "narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=", 68 | "owner": "rust-lang", 69 | "repo": "rust-analyzer", 70 | "rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4", 71 | "type": "github" 72 | }, 73 | "original": { 74 | "owner": "rust-lang", 75 | "ref": "nightly", 76 | "repo": "rust-analyzer", 77 | "type": "github" 78 | } 79 | } 80 | }, 81 | "root": "root", 82 | "version": 7 83 | } 84 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "CLI to manage emails"; 3 | 4 | inputs = { 5 | # FIXME: when #358989 lands on nixos-unstable 6 | # https://nixpk.gs/pr-tracker.html?pr=358989 7 | nixpkgs.url = "github:nixos/nixpkgs/staging-next"; 8 | fenix = { 9 | # TODO: https://github.com/nix-community/fenix/pull/145 10 | # url = "github:nix-community/fenix"; 11 | url = "github:soywod/fenix"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | }; 14 | pimalaya = { 15 | url = "github:pimalaya/nix"; 16 | flake = false; 17 | }; 18 | }; 19 | 20 | outputs = 21 | inputs: 22 | (import inputs.pimalaya).mkFlakeOutputs inputs { 23 | shell = ./shell.nix; 24 | default = ./default.nix; 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | die() { 6 | printf '%s\n' "$1" >&2 7 | exit "${2-1}" 8 | } 9 | 10 | DESTDIR="${DESTDIR:-}" 11 | PREFIX="${PREFIX:-"$DESTDIR/usr/local"}" 12 | RELEASES_URL="https://github.com/pimalaya/himalaya/releases" 13 | 14 | binary=himalaya 15 | system=$(uname -s | tr [:upper:] [:lower:]) 16 | machine=$(uname -m | tr [:upper:] [:lower:]) 17 | 18 | case $system in 19 | msys*|mingw*|cygwin*|win*) 20 | target=x86_64-windows 21 | binary=himalaya.exe;; 22 | 23 | linux|freebsd) 24 | case $machine in 25 | x86_64) target=x86_64-linux;; 26 | x86|i386|i686) target=i686-linux;; 27 | arm64|aarch64) target=aarch64-linux;; 28 | armv6l) target=armv6l-linux;; 29 | armv7l) target=armv7l-linux;; 30 | *) die "Unsupported machine $machine for system $system";; 31 | esac;; 32 | 33 | darwin) 34 | case $machine in 35 | x86_64) target=x86_64-darwin;; 36 | arm64|aarch64) target=aarch64-darwin;; 37 | *) die "Unsupported machine $machine for system $system";; 38 | esac;; 39 | 40 | *) 41 | die "Unsupported system $system";; 42 | esac 43 | 44 | tmpdir=$(mktemp -d) || die "Cannot create temporary directory" 45 | trap "rm -rf $tmpdir" EXIT 46 | 47 | echo "Downloading latest $system release…" 48 | curl -sLo "$tmpdir/himalaya.tgz" \ 49 | "$RELEASES_URL/latest/download/himalaya.$target.tgz" 50 | 51 | echo "Installing binary…" 52 | tar -xzf "$tmpdir/himalaya.tgz" -C "$tmpdir" 53 | 54 | mkdir -p "$PREFIX/bin" 55 | cp -f -- "$tmpdir/$binary" "$PREFIX/bin/$binary" 56 | 57 | die "$("$PREFIX/bin/$binary" --version) installed!" 0 58 | -------------------------------------------------------------------------------- /logo-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | # TODO: move this to nixpkgs 2 | # This file aims to be a replacement for the nixpkgs derivation. 3 | 4 | { 5 | lib, 6 | pkg-config, 7 | rustPlatform, 8 | fetchFromGitHub, 9 | stdenv, 10 | apple-sdk, 11 | installShellFiles, 12 | installShellCompletions ? stdenv.buildPlatform.canExecute stdenv.hostPlatform, 13 | installManPages ? stdenv.buildPlatform.canExecute stdenv.hostPlatform, 14 | notmuch, 15 | gpgme, 16 | buildNoDefaultFeatures ? false, 17 | buildFeatures ? [ ], 18 | }: 19 | 20 | let 21 | version = "1.0.0-beta.4"; 22 | hash = "sha256-NrWBg0sjaz/uLsNs8/T4MkUgHOUvAWRix1O5usKsw6o="; 23 | cargoHash = "sha256-YS8IamapvmdrOPptQh2Ef9Yold0IK1XIeGs0kDIQ5b8="; 24 | in 25 | 26 | rustPlatform.buildRustPackage rec { 27 | inherit cargoHash version; 28 | inherit buildNoDefaultFeatures buildFeatures; 29 | 30 | pname = "himalaya"; 31 | 32 | src = fetchFromGitHub { 33 | inherit hash; 34 | owner = "pimalaya"; 35 | repo = "himalaya"; 36 | rev = "v${version}"; 37 | }; 38 | 39 | nativeBuildInputs = [ 40 | pkg-config 41 | ] ++ lib.optional (installManPages || installShellCompletions) installShellFiles; 42 | 43 | buildInputs = 44 | [ ] 45 | ++ lib.optional stdenv.hostPlatform.isDarwin apple-sdk 46 | ++ lib.optional (builtins.elem "notmuch" buildFeatures) notmuch 47 | ++ lib.optional (builtins.elem "pgp-gpg" buildFeatures) gpgme; 48 | 49 | doCheck = false; 50 | auditable = false; 51 | 52 | # unit tests only 53 | cargoTestFlags = [ "--lib" ]; 54 | 55 | postInstall = 56 | '' 57 | mkdir -p $out/share/{applications,completions,man} 58 | cp assets/himalaya.desktop "$out"/share/applications/ 59 | '' 60 | + lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) '' 61 | "$out"/bin/himalaya man "$out"/share/man 62 | '' 63 | + lib.optionalString installManPages '' 64 | installManPage "$out"/share/man/* 65 | '' 66 | + lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) '' 67 | "$out"/bin/himalaya completion bash > "$out"/share/completions/himalaya.bash 68 | "$out"/bin/himalaya completion elvish > "$out"/share/completions/himalaya.elvish 69 | "$out"/bin/himalaya completion fish > "$out"/share/completions/himalaya.fish 70 | "$out"/bin/himalaya completion powershell > "$out"/share/completions/himalaya.powershell 71 | "$out"/bin/himalaya completion zsh > "$out"/share/completions/himalaya.zsh 72 | '' 73 | + lib.optionalString installShellCompletions '' 74 | installShellCompletion "$out"/share/completions/himalaya.{bash,fish,zsh} 75 | ''; 76 | 77 | meta = rec { 78 | description = "CLI to manage emails"; 79 | mainProgram = "himalaya"; 80 | homepage = "https://github.com/pimalaya/himalaya"; 81 | changelog = "${homepage}/blob/v${version}/CHANGELOG.md"; 82 | license = lib.licenses.mit; 83 | maintainers = with lib.maintainers; [ 84 | soywod 85 | toastal 86 | yanganto 87 | ]; 88 | }; 89 | } 90 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.82.0" 3 | profile = "default" 4 | components = ["rust-src", "rust-analyzer"] 5 | -------------------------------------------------------------------------------- /screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pimalaya/himalaya/cf008c0ca7aec69e3263c63e7adbbed9e380c549/screenshot.jpeg -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { 2 | pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"), 3 | ... 4 | }@args: 5 | 6 | pimalaya.mkShell ({ rustToolchainFile = ./rust-toolchain.toml; } // removeAttrs args [ "pimalaya" ]) 7 | -------------------------------------------------------------------------------- /src/account/arg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod name; 2 | -------------------------------------------------------------------------------- /src/account/arg/name.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// The account name argument parser. 4 | #[derive(Debug, Parser)] 5 | pub struct AccountNameArg { 6 | /// The name of the account. 7 | /// 8 | /// An account name corresponds to an entry in the table at the 9 | /// root level of your TOML configuration file. 10 | #[arg(name = "account_name", value_name = "ACCOUNT")] 11 | pub name: String, 12 | } 13 | 14 | /// The optional account name argument parser. 15 | #[derive(Debug, Parser)] 16 | pub struct OptionalAccountNameArg { 17 | /// The name of the account. 18 | /// 19 | /// An account name corresponds to an entry in the table at the 20 | /// root level of your TOML configuration file. 21 | /// 22 | /// If omitted, the account marked as default will be used. 23 | #[arg(name = "account_name", value_name = "ACCOUNT")] 24 | pub name: Option, 25 | } 26 | 27 | /// The account name flag parser. 28 | #[derive(Debug, Default, Parser)] 29 | pub struct AccountNameFlag { 30 | /// Override the default account. 31 | /// 32 | /// An account name corresponds to an entry in the table at the 33 | /// root level of your TOML configuration file. 34 | #[arg(long = "account", short = 'a')] 35 | #[arg(name = "account_name", value_name = "NAME")] 36 | pub name: Option, 37 | } 38 | -------------------------------------------------------------------------------- /src/account/command/configure.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | 6 | use crate::{account::arg::name::AccountNameArg, config::TomlConfig}; 7 | 8 | /// Configure the given account. 9 | /// 10 | /// This command allows you to configure an existing account or to 11 | /// create a new one, using the wizard. The `wizard` cargo feature is 12 | /// required. 13 | #[derive(Debug, Parser)] 14 | pub struct AccountConfigureCommand { 15 | #[command(flatten)] 16 | pub account: AccountNameArg, 17 | } 18 | 19 | impl AccountConfigureCommand { 20 | #[cfg(feature = "wizard")] 21 | pub async fn execute( 22 | self, 23 | mut config: TomlConfig, 24 | config_path: Option<&PathBuf>, 25 | ) -> Result<()> { 26 | use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _}; 27 | use tracing::info; 28 | 29 | info!("executing account configure command"); 30 | 31 | let path = match config_path { 32 | Some(path) => path.clone(), 33 | None => TomlConfig::default_path()?, 34 | }; 35 | 36 | let account_name = Some(self.account.name.as_str()); 37 | 38 | let account_config = config 39 | .accounts 40 | .remove(&self.account.name) 41 | .unwrap_or_default(); 42 | 43 | wizard::edit(path, config, account_name, account_config).await?; 44 | 45 | Ok(()) 46 | } 47 | 48 | #[cfg(not(feature = "wizard"))] 49 | pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> { 50 | color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/account/command/doctor.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{stdout, Write}, 3 | sync::Arc, 4 | }; 5 | 6 | use clap::Parser; 7 | use color_eyre::{Result, Section}; 8 | #[cfg(all(feature = "keyring", feature = "imap"))] 9 | use email::imap::config::ImapAuthConfig; 10 | #[cfg(feature = "imap")] 11 | use email::imap::ImapContextBuilder; 12 | #[cfg(feature = "maildir")] 13 | use email::maildir::MaildirContextBuilder; 14 | #[cfg(feature = "notmuch")] 15 | use email::notmuch::NotmuchContextBuilder; 16 | #[cfg(feature = "sendmail")] 17 | use email::sendmail::SendmailContextBuilder; 18 | #[cfg(all(feature = "keyring", feature = "smtp"))] 19 | use email::smtp::config::SmtpAuthConfig; 20 | #[cfg(feature = "smtp")] 21 | use email::smtp::SmtpContextBuilder; 22 | use email::{backend::BackendBuilder, config::Config}; 23 | #[cfg(feature = "keyring")] 24 | use pimalaya_tui::terminal::prompt; 25 | use pimalaya_tui::{ 26 | himalaya::config::{Backend, SendingBackend}, 27 | terminal::config::TomlConfig as _, 28 | }; 29 | 30 | use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig}; 31 | 32 | /// Diagnose and fix the given account. 33 | /// 34 | /// This command diagnoses the given account and can even try to fix 35 | /// it. It mostly checks if the configuration is valid, if backends 36 | /// can be instanciated and if sessions work as expected. 37 | #[derive(Debug, Parser)] 38 | pub struct AccountDoctorCommand { 39 | #[command(flatten)] 40 | pub account: OptionalAccountNameArg, 41 | 42 | /// Try to fix the given account. 43 | /// 44 | /// This argument can be used to (re)configure keyring entries for 45 | /// example. 46 | #[arg(long, short)] 47 | pub fix: bool, 48 | } 49 | 50 | impl AccountDoctorCommand { 51 | pub async fn execute(self, config: &TomlConfig) -> Result<()> { 52 | let mut stdout = stdout(); 53 | 54 | if let Some(name) = self.account.name.as_ref() { 55 | print!("Checking TOML configuration integrity for account {name}… "); 56 | } else { 57 | print!("Checking TOML configuration integrity for default account… "); 58 | } 59 | 60 | stdout.flush()?; 61 | 62 | let (toml_account_config, account_config) = config 63 | .clone() 64 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 65 | c.account(name).ok() 66 | })?; 67 | let account_config = Arc::new(account_config); 68 | 69 | println!("OK"); 70 | 71 | #[cfg(feature = "keyring")] 72 | if self.fix { 73 | if prompt::bool("Would you like to reset existing keyring entries?", false)? { 74 | print!("Resetting keyring entries… "); 75 | stdout.flush()?; 76 | 77 | #[cfg(feature = "imap")] 78 | match toml_account_config.imap_auth_config() { 79 | Some(ImapAuthConfig::Password(config)) => config.reset().await?, 80 | #[cfg(feature = "oauth2")] 81 | Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?, 82 | _ => (), 83 | } 84 | 85 | #[cfg(feature = "smtp")] 86 | match toml_account_config.smtp_auth_config() { 87 | Some(SmtpAuthConfig::Password(config)) => config.reset().await?, 88 | #[cfg(feature = "oauth2")] 89 | Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?, 90 | _ => (), 91 | } 92 | 93 | #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] 94 | if let Some(config) = &toml_account_config.pgp { 95 | config.reset().await?; 96 | } 97 | 98 | println!("OK"); 99 | } 100 | 101 | #[cfg(feature = "imap")] 102 | match toml_account_config.imap_auth_config() { 103 | Some(ImapAuthConfig::Password(config)) => { 104 | config 105 | .configure(|| Ok(prompt::password("IMAP password")?)) 106 | .await?; 107 | } 108 | #[cfg(feature = "oauth2")] 109 | Some(ImapAuthConfig::OAuth2(config)) => { 110 | config 111 | .configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?)) 112 | .await?; 113 | } 114 | _ => (), 115 | }; 116 | 117 | #[cfg(feature = "smtp")] 118 | match toml_account_config.smtp_auth_config() { 119 | Some(SmtpAuthConfig::Password(config)) => { 120 | config 121 | .configure(|| Ok(prompt::password("SMTP password")?)) 122 | .await?; 123 | } 124 | #[cfg(feature = "oauth2")] 125 | Some(SmtpAuthConfig::OAuth2(config)) => { 126 | config 127 | .configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?)) 128 | .await?; 129 | } 130 | _ => (), 131 | }; 132 | 133 | #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] 134 | if let Some(config) = &toml_account_config.pgp { 135 | config 136 | .configure(&toml_account_config.email, || { 137 | Ok(prompt::password("PGP secret key password")?) 138 | }) 139 | .await?; 140 | } 141 | } 142 | 143 | match toml_account_config.backend { 144 | #[cfg(feature = "maildir")] 145 | Some(Backend::Maildir(mdir_config)) => { 146 | print!("Checking Maildir integrity… "); 147 | stdout.flush()?; 148 | 149 | let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config)); 150 | BackendBuilder::new(account_config.clone(), ctx) 151 | .check_up() 152 | .await?; 153 | 154 | println!("OK"); 155 | } 156 | #[cfg(feature = "imap")] 157 | Some(Backend::Imap(imap_config)) => { 158 | print!("Checking IMAP integrity… "); 159 | stdout.flush()?; 160 | 161 | let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config)) 162 | .with_pool_size(1); 163 | let res = BackendBuilder::new(account_config.clone(), ctx) 164 | .check_up() 165 | .await; 166 | 167 | if self.fix { 168 | res?; 169 | } else { 170 | res.note("Run with --fix to (re)configure your account.")?; 171 | } 172 | 173 | println!("OK"); 174 | } 175 | #[cfg(feature = "notmuch")] 176 | Some(Backend::Notmuch(notmuch_config)) => { 177 | print!("Checking Notmuch integrity… "); 178 | stdout.flush()?; 179 | 180 | let ctx = 181 | NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config)); 182 | BackendBuilder::new(account_config.clone(), ctx) 183 | .check_up() 184 | .await?; 185 | 186 | println!("OK"); 187 | } 188 | _ => (), 189 | } 190 | 191 | let sending_backend = toml_account_config 192 | .message 193 | .and_then(|msg| msg.send) 194 | .and_then(|send| send.backend); 195 | 196 | match sending_backend { 197 | #[cfg(feature = "smtp")] 198 | Some(SendingBackend::Smtp(smtp_config)) => { 199 | print!("Checking SMTP integrity… "); 200 | stdout.flush()?; 201 | 202 | let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config)); 203 | let res = BackendBuilder::new(account_config.clone(), ctx) 204 | .check_up() 205 | .await; 206 | 207 | if self.fix { 208 | res?; 209 | } else { 210 | res.note("Run with --fix to (re)configure your account.")?; 211 | } 212 | 213 | println!("OK"); 214 | } 215 | #[cfg(feature = "sendmail")] 216 | Some(SendingBackend::Sendmail(sendmail_config)) => { 217 | print!("Checking Sendmail integrity… "); 218 | stdout.flush()?; 219 | 220 | let ctx = 221 | SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config)); 222 | BackendBuilder::new(account_config.clone(), ctx) 223 | .check_up() 224 | .await?; 225 | 226 | println!("OK"); 227 | } 228 | _ => (), 229 | } 230 | 231 | Ok(()) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/account/command/list.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use pimalaya_tui::{ 4 | himalaya::config::{Accounts, AccountsTable}, 5 | terminal::cli::printer::Printer, 6 | }; 7 | use tracing::info; 8 | 9 | use crate::config::TomlConfig; 10 | 11 | /// List all existing accounts. 12 | /// 13 | /// This command lists all the accounts defined in your TOML 14 | /// configuration file. 15 | #[derive(Debug, Parser)] 16 | pub struct AccountListCommand { 17 | /// The maximum width the table should not exceed. 18 | /// 19 | /// This argument will force the table not to exceed the given 20 | /// width, in pixels. Columns may shrink with ellipsis in order to 21 | /// fit the width. 22 | #[arg(long = "max-width", short = 'w')] 23 | #[arg(name = "table_max_width", value_name = "PIXELS")] 24 | pub table_max_width: Option, 25 | } 26 | 27 | impl AccountListCommand { 28 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 29 | info!("executing list accounts command"); 30 | 31 | let accounts = Accounts::from(config.accounts.iter()); 32 | let table = AccountsTable::from(accounts) 33 | .with_some_width(self.table_max_width) 34 | .with_some_preset(config.account_list_table_preset()) 35 | .with_some_name_color(config.account_list_table_name_color()) 36 | .with_some_backends_color(config.account_list_table_backends_color()) 37 | .with_some_default_color(config.account_list_table_default_color()); 38 | 39 | printer.out(table) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/account/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod configure; 2 | mod doctor; 3 | mod list; 4 | 5 | use std::path::PathBuf; 6 | 7 | use clap::Subcommand; 8 | use color_eyre::Result; 9 | use pimalaya_tui::terminal::cli::printer::Printer; 10 | 11 | use crate::config::TomlConfig; 12 | 13 | use self::{ 14 | configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand, 15 | }; 16 | 17 | /// Configure, list and diagnose your accounts. 18 | /// 19 | /// An account is a group of settings, identified by a unique 20 | /// name. This subcommand allows you to manage your accounts. 21 | #[derive(Debug, Subcommand)] 22 | pub enum AccountSubcommand { 23 | Configure(AccountConfigureCommand), 24 | Doctor(AccountDoctorCommand), 25 | List(AccountListCommand), 26 | } 27 | 28 | impl AccountSubcommand { 29 | pub async fn execute( 30 | self, 31 | printer: &mut impl Printer, 32 | config: TomlConfig, 33 | config_path: Option<&PathBuf>, 34 | ) -> Result<()> { 35 | match self { 36 | Self::Configure(cmd) => cmd.execute(config, config_path).await, 37 | Self::Doctor(cmd) => cmd.execute(&config).await, 38 | Self::List(cmd) => cmd.execute(printer, &config).await, 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/account/config.rs: -------------------------------------------------------------------------------- 1 | use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig; 2 | 3 | pub type TomlAccountConfig = HimalayaTomlAccountConfig; 4 | -------------------------------------------------------------------------------- /src/account/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod command; 3 | pub mod config; 4 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Parser, Subcommand}; 4 | use color_eyre::Result; 5 | use pimalaya_tui::{ 6 | long_version, 7 | terminal::{ 8 | cli::{ 9 | arg::path_parser, 10 | printer::{OutputFmt, Printer}, 11 | }, 12 | config::TomlConfig as _, 13 | }, 14 | }; 15 | 16 | use crate::{ 17 | account::command::AccountSubcommand, 18 | completion::command::CompletionGenerateCommand, 19 | config::TomlConfig, 20 | envelope::command::EnvelopeSubcommand, 21 | flag::command::FlagSubcommand, 22 | folder::command::FolderSubcommand, 23 | manual::command::ManualGenerateCommand, 24 | message::{ 25 | attachment::command::AttachmentSubcommand, command::MessageSubcommand, 26 | template::command::TemplateSubcommand, 27 | }, 28 | }; 29 | 30 | #[derive(Parser, Debug)] 31 | #[command(name = env!("CARGO_PKG_NAME"))] 32 | #[command(author, version, about)] 33 | #[command(long_version = long_version!())] 34 | #[command(propagate_version = true, infer_subcommands = true)] 35 | pub struct Cli { 36 | #[command(subcommand)] 37 | pub command: Option, 38 | 39 | /// Override the default configuration file path. 40 | /// 41 | /// The given paths are shell-expanded then canonicalized (if 42 | /// applicable). If the first path does not point to a valid file, 43 | /// the wizard will propose to assist you in the creation of the 44 | /// configuration file. Other paths are merged with the first one, 45 | /// which allows you to separate your public config from your 46 | /// private(s) one(s). 47 | #[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")] 48 | #[arg(value_name = "PATH", value_parser = path_parser)] 49 | pub config_paths: Vec, 50 | 51 | /// Customize the output format. 52 | /// 53 | /// The output format determine how to display commands output to 54 | /// the terminal. 55 | /// 56 | /// The possible values are: 57 | /// 58 | /// - json: output will be in a form of a JSON-compatible object 59 | /// 60 | /// - plain: output will be in a form of either a plain text or 61 | /// table, depending on the command 62 | #[arg(long, short, global = true)] 63 | #[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())] 64 | pub output: OutputFmt, 65 | 66 | /// Enable logs with spantrace. 67 | /// 68 | /// This is the same as running the command with `RUST_LOG=debug` 69 | /// environment variable. 70 | #[arg(long, global = true, conflicts_with = "trace")] 71 | pub debug: bool, 72 | 73 | /// Enable verbose logs with backtrace. 74 | /// 75 | /// This is the same as running the command with `RUST_LOG=trace` 76 | /// and `RUST_BACKTRACE=1` environment variables. 77 | #[arg(long, global = true, conflicts_with = "debug")] 78 | pub trace: bool, 79 | } 80 | 81 | #[derive(Subcommand, Debug)] 82 | pub enum HimalayaCommand { 83 | #[command(subcommand)] 84 | #[command(alias = "accounts")] 85 | Account(AccountSubcommand), 86 | 87 | #[command(subcommand)] 88 | #[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])] 89 | #[command(alias = "folders")] 90 | Folder(FolderSubcommand), 91 | 92 | #[command(subcommand)] 93 | #[command(alias = "envelopes")] 94 | Envelope(EnvelopeSubcommand), 95 | 96 | #[command(subcommand)] 97 | #[command(alias = "flags")] 98 | Flag(FlagSubcommand), 99 | 100 | #[command(subcommand)] 101 | #[command(alias = "messages", alias = "msgs", alias = "msg")] 102 | Message(MessageSubcommand), 103 | 104 | #[command(subcommand)] 105 | #[command(alias = "attachments")] 106 | Attachment(AttachmentSubcommand), 107 | 108 | #[command(subcommand)] 109 | #[command(alias = "templates", alias = "tpls", alias = "tpl")] 110 | Template(TemplateSubcommand), 111 | 112 | #[command(arg_required_else_help = true)] 113 | #[command(alias = "manuals", alias = "mans")] 114 | Manual(ManualGenerateCommand), 115 | 116 | #[command(arg_required_else_help = true)] 117 | #[command(alias = "completions")] 118 | Completion(CompletionGenerateCommand), 119 | } 120 | 121 | impl HimalayaCommand { 122 | pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> { 123 | match self { 124 | Self::Account(cmd) => { 125 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 126 | cmd.execute(printer, config, config_paths.first()).await 127 | } 128 | Self::Folder(cmd) => { 129 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 130 | cmd.execute(printer, &config).await 131 | } 132 | Self::Envelope(cmd) => { 133 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 134 | cmd.execute(printer, &config).await 135 | } 136 | Self::Flag(cmd) => { 137 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 138 | cmd.execute(printer, &config).await 139 | } 140 | Self::Message(cmd) => { 141 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 142 | cmd.execute(printer, &config).await 143 | } 144 | Self::Attachment(cmd) => { 145 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 146 | cmd.execute(printer, &config).await 147 | } 148 | Self::Template(cmd) => { 149 | let config = TomlConfig::from_paths_or_default(config_paths).await?; 150 | cmd.execute(printer, &config).await 151 | } 152 | Self::Manual(cmd) => cmd.execute(printer).await, 153 | Self::Completion(cmd) => cmd.execute().await, 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/completion/command.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use clap::{value_parser, CommandFactory, Parser}; 4 | use clap_complete::Shell; 5 | use color_eyre::Result; 6 | use tracing::info; 7 | 8 | use crate::cli::Cli; 9 | 10 | /// Print completion script for the given shell to stdout. 11 | /// 12 | /// This command allows you to generate completion script for a given 13 | /// shell. The script is printed to the standard output. If you want 14 | /// to write it to a file, just use unix redirection. 15 | #[derive(Debug, Parser)] 16 | pub struct CompletionGenerateCommand { 17 | /// Shell for which completion script should be generated for. 18 | #[arg(value_parser = value_parser!(Shell))] 19 | pub shell: Shell, 20 | } 21 | 22 | impl CompletionGenerateCommand { 23 | pub async fn execute(self) -> Result<()> { 24 | info!("executing generate completion command"); 25 | 26 | let mut cmd = Cli::command(); 27 | let name = cmd.get_name().to_string(); 28 | clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout()); 29 | 30 | Ok(()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/completion/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use pimalaya_tui::himalaya::config::HimalayaTomlConfig; 2 | 3 | pub type TomlConfig = HimalayaTomlConfig; 4 | -------------------------------------------------------------------------------- /src/email/envelope/arg/ids.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// The envelope id argument parser. 4 | #[derive(Debug, Parser)] 5 | pub struct EnvelopeIdArg { 6 | /// The envelope id. 7 | #[arg(value_name = "ID", required = true)] 8 | pub id: usize, 9 | } 10 | 11 | /// The envelopes ids arguments parser. 12 | #[derive(Debug, Parser)] 13 | pub struct EnvelopeIdsArgs { 14 | /// The list of envelopes ids. 15 | #[arg(value_name = "ID", required = true)] 16 | pub ids: Vec, 17 | } 18 | -------------------------------------------------------------------------------- /src/email/envelope/arg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ids; 2 | -------------------------------------------------------------------------------- /src/email/envelope/command/list.rs: -------------------------------------------------------------------------------- 1 | use std::{process::exit, sync::Arc}; 2 | 3 | use ariadne::{Color, Label, Report, ReportKind, Source}; 4 | use clap::Parser; 5 | use color_eyre::Result; 6 | use email::{ 7 | backend::feature::BackendFeatureSource, config::Config, email::search_query, 8 | envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, 9 | }; 10 | use pimalaya_tui::{ 11 | himalaya::{backend::BackendBuilder, config::EnvelopesTable}, 12 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 13 | }; 14 | use tracing::info; 15 | 16 | use crate::{ 17 | account::arg::name::AccountNameFlag, config::TomlConfig, 18 | folder::arg::name::FolderNameOptionalFlag, 19 | }; 20 | 21 | /// Search and sort envelopes as a list. 22 | /// 23 | /// This command allows you to list envelopes included in the given 24 | /// folder, matching the given query. 25 | #[derive(Debug, Parser)] 26 | pub struct EnvelopeListCommand { 27 | #[command(flatten)] 28 | pub folder: FolderNameOptionalFlag, 29 | 30 | /// The page number. 31 | /// 32 | /// The page number starts from 1 (which is the default). Giving a 33 | /// page number to big will result in a out of bound error. 34 | #[arg(long, short, value_name = "NUMBER", default_value = "1")] 35 | pub page: usize, 36 | 37 | /// The page size. 38 | /// 39 | /// Determine the amount of envelopes a page should contain. 40 | #[arg(long, short = 's', value_name = "NUMBER")] 41 | pub page_size: Option, 42 | 43 | #[command(flatten)] 44 | pub account: AccountNameFlag, 45 | 46 | /// The maximum width the table should not exceed. 47 | /// 48 | /// This argument will force the table not to exceed the given 49 | /// width in pixels. Columns may shrink with ellipsis in order to 50 | /// fit the width. 51 | #[arg(long = "max-width", short = 'w')] 52 | #[arg(name = "table_max_width", value_name = "PIXELS")] 53 | pub table_max_width: Option, 54 | 55 | /// The list envelopes filter and sort query. 56 | /// 57 | /// The query can be a filter query, a sort query or both 58 | /// together. 59 | /// 60 | /// A filter query is composed of operators and conditions. There 61 | /// is 3 operators and 8 conditions: 62 | /// 63 | /// • not → filter envelopes that do not match the 64 | /// condition 65 | /// 66 | /// • and → filter envelopes that match 67 | /// both conditions 68 | /// 69 | /// • or → filter envelopes that match 70 | /// one of the conditions 71 | /// 72 | /// ◦ date → filter envelopes that match the given 73 | /// date 74 | /// 75 | /// ◦ before → filter envelopes with date strictly 76 | /// before the given one 77 | /// 78 | /// ◦ after → filter envelopes with date stricly 79 | /// after the given one 80 | /// 81 | /// ◦ from → filter envelopes with senders matching the 82 | /// given pattern 83 | /// 84 | /// ◦ to → filter envelopes with recipients matching 85 | /// the given pattern 86 | /// 87 | /// ◦ subject → filter envelopes with subject matching 88 | /// the given pattern 89 | /// 90 | /// ◦ body → filter envelopes with text bodies matching 91 | /// the given pattern 92 | /// 93 | /// ◦ flag → filter envelopes matching the given flag 94 | /// 95 | /// A sort query starts by "order by", and is composed of kinds 96 | /// and orders. There is 4 kinds and 2 orders: 97 | /// 98 | /// • date [order] → sort envelopes by date 99 | /// 100 | /// • from [order] → sort envelopes by sender 101 | /// 102 | /// • to [order] → sort envelopes by recipient 103 | /// 104 | /// • subject [order] → sort envelopes by subject 105 | /// 106 | /// ◦ asc → sort envelopes by the given kind in ascending 107 | /// order 108 | /// 109 | /// ◦ desc → sort envelopes by the given kind in 110 | /// descending order 111 | /// 112 | /// Examples: 113 | /// 114 | /// subject foo and body bar → filter envelopes containing "foo" 115 | /// in their subject and "bar" in their text bodies 116 | /// 117 | /// order by date desc subject → sort envelopes by descending date 118 | /// (most recent first), then by ascending subject 119 | /// 120 | /// subject foo and body bar order by date desc subject → 121 | /// combination of the 2 previous examples 122 | #[arg(allow_hyphen_values = true, trailing_var_arg = true)] 123 | pub query: Option>, 124 | } 125 | 126 | impl Default for EnvelopeListCommand { 127 | fn default() -> Self { 128 | Self { 129 | folder: Default::default(), 130 | page: 1, 131 | page_size: Default::default(), 132 | account: Default::default(), 133 | query: Default::default(), 134 | table_max_width: Default::default(), 135 | } 136 | } 137 | } 138 | 139 | impl EnvelopeListCommand { 140 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 141 | info!("executing list envelopes command"); 142 | 143 | let (toml_account_config, account_config) = config 144 | .clone() 145 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 146 | c.account(name).ok() 147 | })?; 148 | 149 | let toml_account_config = Arc::new(toml_account_config); 150 | 151 | let folder = &self.folder.name; 152 | let page = 1.max(self.page) - 1; 153 | let page_size = self 154 | .page_size 155 | .unwrap_or_else(|| account_config.get_envelope_list_page_size()); 156 | 157 | let backend = BackendBuilder::new( 158 | toml_account_config.clone(), 159 | Arc::new(account_config), 160 | |builder| { 161 | builder 162 | .without_features() 163 | .with_list_envelopes(BackendFeatureSource::Context) 164 | }, 165 | ) 166 | .without_sending_backend() 167 | .build() 168 | .await?; 169 | 170 | let query = self 171 | .query 172 | .map(|query| query.join(" ").parse::()); 173 | let query = match query { 174 | None => None, 175 | Some(Ok(query)) => Some(query), 176 | Some(Err(main_err)) => { 177 | let source = "query"; 178 | let search_query::error::Error::ParseError(errs, query) = &main_err; 179 | for err in errs { 180 | Report::build(ReportKind::Error, source, err.span().start) 181 | .with_message(main_err.to_string()) 182 | .with_label( 183 | Label::new((source, err.span().into_range())) 184 | .with_message(err.reason().to_string()) 185 | .with_color(Color::Red), 186 | ) 187 | .finish() 188 | .eprint((source, Source::from(&query))) 189 | .unwrap(); 190 | } 191 | 192 | exit(0) 193 | } 194 | }; 195 | 196 | let opts = ListEnvelopesOptions { 197 | page, 198 | page_size, 199 | query, 200 | }; 201 | 202 | let envelopes = backend.list_envelopes(folder, opts).await?; 203 | let table = EnvelopesTable::from(envelopes) 204 | .with_some_width(self.table_max_width) 205 | .with_some_preset(toml_account_config.envelope_list_table_preset()) 206 | .with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char()) 207 | .with_some_replied_char(toml_account_config.envelope_list_table_replied_char()) 208 | .with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char()) 209 | .with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char()) 210 | .with_some_id_color(toml_account_config.envelope_list_table_id_color()) 211 | .with_some_flags_color(toml_account_config.envelope_list_table_flags_color()) 212 | .with_some_subject_color(toml_account_config.envelope_list_table_subject_color()) 213 | .with_some_sender_color(toml_account_config.envelope_list_table_sender_color()) 214 | .with_some_date_color(toml_account_config.envelope_list_table_date_color()); 215 | 216 | printer.out(table) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /src/email/envelope/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod list; 2 | pub mod thread; 3 | 4 | use clap::Subcommand; 5 | use color_eyre::Result; 6 | use pimalaya_tui::terminal::cli::printer::Printer; 7 | 8 | use crate::config::TomlConfig; 9 | 10 | use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand}; 11 | 12 | /// List, search and sort your envelopes. 13 | /// 14 | /// An envelope is a small representation of a message. It contains an 15 | /// identifier (given by the backend), some flags as well as few 16 | /// headers from the message itself. This subcommand allows you to 17 | /// manage them. 18 | #[derive(Debug, Subcommand)] 19 | pub enum EnvelopeSubcommand { 20 | #[command(alias = "lst")] 21 | List(EnvelopeListCommand), 22 | 23 | #[command()] 24 | Thread(EnvelopeThreadCommand), 25 | } 26 | 27 | impl EnvelopeSubcommand { 28 | #[allow(unused)] 29 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 30 | match self { 31 | Self::List(cmd) => cmd.execute(printer, config).await, 32 | Self::Thread(cmd) => cmd.execute(printer, config).await, 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/email/envelope/command/thread.rs: -------------------------------------------------------------------------------- 1 | use ariadne::{Label, Report, ReportKind, Source}; 2 | use clap::Parser; 3 | use color_eyre::Result; 4 | use email::{ 5 | backend::feature::BackendFeatureSource, config::Config, email::search_query, 6 | envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery, 7 | }; 8 | use pimalaya_tui::{ 9 | himalaya::{backend::BackendBuilder, config::EnvelopesTree}, 10 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 11 | }; 12 | use std::{process::exit, sync::Arc}; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, config::TomlConfig, 17 | folder::arg::name::FolderNameOptionalFlag, 18 | }; 19 | 20 | /// Search and sort envelopes as a thread. 21 | /// 22 | /// This command allows you to thread envelopes included in the given 23 | /// folder, matching the given query. 24 | #[derive(Debug, Parser)] 25 | pub struct EnvelopeThreadCommand { 26 | #[command(flatten)] 27 | pub folder: FolderNameOptionalFlag, 28 | 29 | #[command(flatten)] 30 | pub account: AccountNameFlag, 31 | 32 | /// Show only threads that contain the given envelope identifier. 33 | #[arg(long, short)] 34 | pub id: Option, 35 | 36 | /// The list envelopes filter and sort query. 37 | /// 38 | /// See `envelope list --help` for more information. 39 | #[arg(allow_hyphen_values = true, trailing_var_arg = true)] 40 | pub query: Option>, 41 | } 42 | 43 | impl EnvelopeThreadCommand { 44 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 45 | info!("executing thread envelopes command"); 46 | 47 | let (toml_account_config, account_config) = config 48 | .clone() 49 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 50 | c.account(name).ok() 51 | })?; 52 | 53 | let account_config = Arc::new(account_config); 54 | let folder = &self.folder.name; 55 | 56 | let backend = BackendBuilder::new( 57 | Arc::new(toml_account_config), 58 | account_config.clone(), 59 | |builder| { 60 | builder 61 | .without_features() 62 | .with_thread_envelopes(BackendFeatureSource::Context) 63 | }, 64 | ) 65 | .without_sending_backend() 66 | .build() 67 | .await?; 68 | 69 | let query = self 70 | .query 71 | .map(|query| query.join(" ").parse::()); 72 | let query = match query { 73 | None => None, 74 | Some(Ok(query)) => Some(query), 75 | Some(Err(main_err)) => { 76 | let source = "query"; 77 | let search_query::error::Error::ParseError(errs, query) = &main_err; 78 | for err in errs { 79 | Report::build(ReportKind::Error, source, err.span().start) 80 | .with_message(main_err.to_string()) 81 | .with_label( 82 | Label::new((source, err.span().into_range())) 83 | .with_message(err.reason().to_string()) 84 | .with_color(ariadne::Color::Red), 85 | ) 86 | .finish() 87 | .eprint((source, Source::from(&query))) 88 | .unwrap(); 89 | } 90 | 91 | exit(0) 92 | } 93 | }; 94 | 95 | let opts = ListEnvelopesOptions { 96 | page: 0, 97 | page_size: 0, 98 | query, 99 | }; 100 | 101 | let envelopes = match self.id { 102 | Some(id) => backend.thread_envelope(folder, id, opts).await, 103 | None => backend.thread_envelopes(folder, opts).await, 104 | }?; 105 | 106 | let tree = EnvelopesTree::new(account_config, envelopes); 107 | 108 | printer.out(tree) 109 | } 110 | } 111 | 112 | // #[cfg(test)] 113 | // mod test { 114 | // use email::{account::config::AccountConfig, envelope::ThreadedEnvelope}; 115 | // use petgraph::graphmap::DiGraphMap; 116 | 117 | // use super::write_tree; 118 | 119 | // macro_rules! e { 120 | // ($id:literal) => { 121 | // ThreadedEnvelope { 122 | // id: $id, 123 | // message_id: $id, 124 | // from: "", 125 | // subject: "", 126 | // date: Default::default(), 127 | // } 128 | // }; 129 | // } 130 | 131 | // #[test] 132 | // fn tree_1() { 133 | // let config = AccountConfig::default(); 134 | // let mut buf = Vec::new(); 135 | // let mut graph = DiGraphMap::new(); 136 | // graph.add_edge(e!("0"), e!("1"), 0); 137 | // graph.add_edge(e!("0"), e!("2"), 0); 138 | // graph.add_edge(e!("0"), e!("3"), 0); 139 | 140 | // write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); 141 | // let buf = String::from_utf8_lossy(&buf); 142 | 143 | // let expected = " 144 | // 0 145 | // ├─ 1 146 | // ├─ 2 147 | // └─ 3 148 | // "; 149 | // assert_eq!(expected.trim_start(), buf) 150 | // } 151 | 152 | // #[test] 153 | // fn tree_2() { 154 | // let config = AccountConfig::default(); 155 | // let mut buf = Vec::new(); 156 | // let mut graph = DiGraphMap::new(); 157 | // graph.add_edge(e!("0"), e!("1"), 0); 158 | // graph.add_edge(e!("1"), e!("2"), 1); 159 | // graph.add_edge(e!("1"), e!("3"), 1); 160 | 161 | // write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); 162 | // let buf = String::from_utf8_lossy(&buf); 163 | 164 | // let expected = " 165 | // 0 166 | // └─ 1 167 | // ├─ 2 168 | // └─ 3 169 | // "; 170 | // assert_eq!(expected.trim_start(), buf) 171 | // } 172 | 173 | // #[test] 174 | // fn tree_3() { 175 | // let config = AccountConfig::default(); 176 | // let mut buf = Vec::new(); 177 | // let mut graph = DiGraphMap::new(); 178 | // graph.add_edge(e!("0"), e!("1"), 0); 179 | // graph.add_edge(e!("1"), e!("2"), 1); 180 | // graph.add_edge(e!("2"), e!("22"), 2); 181 | // graph.add_edge(e!("1"), e!("3"), 1); 182 | // graph.add_edge(e!("0"), e!("4"), 0); 183 | // graph.add_edge(e!("4"), e!("5"), 1); 184 | // graph.add_edge(e!("5"), e!("6"), 2); 185 | 186 | // write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap(); 187 | // let buf = String::from_utf8_lossy(&buf); 188 | 189 | // let expected = " 190 | // 0 191 | // ├─ 1 192 | // │ ├─ 2 193 | // │ │ └─ 22 194 | // │ └─ 3 195 | // └─ 4 196 | // └─ 5 197 | // └─ 6 198 | // "; 199 | // assert_eq!(expected.trim_start(), buf) 200 | // } 201 | // } 202 | -------------------------------------------------------------------------------- /src/email/envelope/flag/arg/ids_and_flags.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use email::flag::{Flag, Flags}; 3 | use tracing::debug; 4 | 5 | /// The ids and/or flags arguments parser. 6 | #[derive(Debug, Parser)] 7 | pub struct IdsAndFlagsArgs { 8 | /// The list of ids and/or flags. 9 | /// 10 | /// Every argument that can be parsed as an integer is considered 11 | /// an id, otherwise it is considered as a flag. 12 | #[arg(value_name = "ID-OR-FLAG", required = true)] 13 | pub ids_and_flags: Vec, 14 | } 15 | 16 | #[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] 17 | pub enum IdOrFlag { 18 | Id(usize), 19 | Flag(Flag), 20 | } 21 | 22 | impl From<&str> for IdOrFlag { 23 | fn from(value: &str) -> Self { 24 | value.parse::().map(Self::Id).unwrap_or_else(|err| { 25 | let flag = Flag::from(value); 26 | debug!("cannot parse {value} as usize, parsing it as flag {flag}"); 27 | debug!("{err:?}"); 28 | Self::Flag(flag) 29 | }) 30 | } 31 | } 32 | 33 | pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec, Flags) { 34 | ids_and_flags.iter().fold( 35 | (Vec::default(), Flags::default()), 36 | |(mut ids, mut flags), arg| { 37 | match arg { 38 | IdOrFlag::Id(id) => { 39 | ids.push(*id); 40 | } 41 | IdOrFlag::Flag(flag) => { 42 | flags.insert(flag.to_owned()); 43 | } 44 | }; 45 | (ids, flags) 46 | }, 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/email/envelope/flag/arg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ids_and_flags; 2 | -------------------------------------------------------------------------------- /src/email/envelope/flag/command/add.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | }; 18 | 19 | /// Add flag(s) to the given envelope. 20 | /// 21 | /// This command allows you to attach the given flag(s) to the given 22 | /// envelope(s). 23 | #[derive(Debug, Parser)] 24 | pub struct FlagAddCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameOptionalFlag, 27 | 28 | #[command(flatten)] 29 | pub args: IdsAndFlagsArgs, 30 | 31 | #[command(flatten)] 32 | pub account: AccountNameFlag, 33 | } 34 | 35 | impl FlagAddCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing add flag(s) command"); 38 | 39 | let folder = &self.folder.name; 40 | let (ids, flags) = into_tuple(&self.args.ids_and_flags); 41 | let (toml_account_config, account_config) = config 42 | .clone() 43 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 44 | c.account(name).ok() 45 | })?; 46 | 47 | let backend = BackendBuilder::new( 48 | Arc::new(toml_account_config), 49 | Arc::new(account_config), 50 | |builder| { 51 | builder 52 | .without_features() 53 | .with_add_flags(BackendFeatureSource::Context) 54 | }, 55 | ) 56 | .without_sending_backend() 57 | .build() 58 | .await?; 59 | 60 | backend.add_flags(folder, &ids, &flags).await?; 61 | 62 | printer.out(format!("Flag(s) {flags} successfully added!\n")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/email/envelope/flag/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod add; 2 | mod remove; 3 | mod set; 4 | 5 | use clap::Subcommand; 6 | use color_eyre::Result; 7 | use pimalaya_tui::terminal::cli::printer::Printer; 8 | 9 | use crate::config::TomlConfig; 10 | 11 | use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand}; 12 | 13 | /// Add, change and remove your envelopes flags. 14 | /// 15 | /// A flag is a tag associated to an envelope. Existing flags are 16 | /// seen, answered, flagged, deleted, draft. Other flags are 17 | /// considered custom, which are not always supported. 18 | #[derive(Debug, Subcommand)] 19 | pub enum FlagSubcommand { 20 | #[command(arg_required_else_help = true)] 21 | #[command(alias = "create")] 22 | Add(FlagAddCommand), 23 | 24 | #[command(arg_required_else_help = true)] 25 | #[command(aliases = ["update", "change", "replace"])] 26 | Set(FlagSetCommand), 27 | 28 | #[command(arg_required_else_help = true)] 29 | #[command(aliases = ["rm", "delete", "del"])] 30 | Remove(FlagRemoveCommand), 31 | } 32 | 33 | impl FlagSubcommand { 34 | #[allow(unused)] 35 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 36 | match self { 37 | Self::Add(cmd) => cmd.execute(printer, config).await, 38 | Self::Set(cmd) => cmd.execute(printer, config).await, 39 | Self::Remove(cmd) => cmd.execute(printer, config).await, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/email/envelope/flag/command/remove.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | }; 18 | 19 | /// Remove flag(s) from a given envelope. 20 | /// 21 | /// This command allows you to remove the given flag(s) from the given 22 | /// envelope(s). 23 | #[derive(Debug, Parser)] 24 | pub struct FlagRemoveCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameOptionalFlag, 27 | 28 | #[command(flatten)] 29 | pub args: IdsAndFlagsArgs, 30 | 31 | #[command(flatten)] 32 | pub account: AccountNameFlag, 33 | } 34 | 35 | impl FlagRemoveCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing remove flag(s) command"); 38 | 39 | let folder = &self.folder.name; 40 | let (ids, flags) = into_tuple(&self.args.ids_and_flags); 41 | let (toml_account_config, account_config) = config 42 | .clone() 43 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 44 | c.account(name).ok() 45 | })?; 46 | 47 | let backend = BackendBuilder::new( 48 | Arc::new(toml_account_config), 49 | Arc::new(account_config), 50 | |builder| { 51 | builder 52 | .without_features() 53 | .with_remove_flags(BackendFeatureSource::Context) 54 | }, 55 | ) 56 | .without_sending_backend() 57 | .build() 58 | .await?; 59 | 60 | backend.remove_flags(folder, &ids, &flags).await?; 61 | 62 | printer.out(format!("Flag(s) {flags} successfully removed!\n")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/email/envelope/flag/command/set.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs}, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | }; 18 | 19 | /// Replace flag(s) of a given envelope. 20 | /// 21 | /// This command allows you to replace existing flags of the given 22 | /// envelope(s) with the given flag(s). 23 | #[derive(Debug, Parser)] 24 | pub struct FlagSetCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameOptionalFlag, 27 | 28 | #[command(flatten)] 29 | pub args: IdsAndFlagsArgs, 30 | 31 | #[command(flatten)] 32 | pub account: AccountNameFlag, 33 | } 34 | 35 | impl FlagSetCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing set flag(s) command"); 38 | 39 | let folder = &self.folder.name; 40 | let (ids, flags) = into_tuple(&self.args.ids_and_flags); 41 | let (toml_account_config, account_config) = config 42 | .clone() 43 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 44 | c.account(name).ok() 45 | })?; 46 | 47 | let backend = BackendBuilder::new( 48 | Arc::new(toml_account_config), 49 | Arc::new(account_config), 50 | |builder| { 51 | builder 52 | .without_features() 53 | .with_set_flags(BackendFeatureSource::Context) 54 | }, 55 | ) 56 | .without_sending_backend() 57 | .build() 58 | .await?; 59 | 60 | backend.set_flags(folder, &ids, &flags).await?; 61 | 62 | printer.out(format!("Flag(s) {flags} successfully replaced!\n")) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/email/envelope/flag/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod command; 3 | -------------------------------------------------------------------------------- /src/email/envelope/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod command; 3 | pub mod flag; 4 | -------------------------------------------------------------------------------- /src/email/message/arg/body.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::ops::Deref; 3 | 4 | /// The raw message body argument parser. 5 | #[derive(Debug, Parser)] 6 | pub struct MessageRawBodyArg { 7 | /// Prefill the template with a custom body. 8 | #[arg(trailing_var_arg = true)] 9 | #[arg(name = "body_raw", value_name = "BODY")] 10 | pub raw: Vec, 11 | } 12 | 13 | impl MessageRawBodyArg { 14 | pub fn raw(self) -> String { 15 | self.raw.join(" ").replace('\r', "").replace('\n', "\r\n") 16 | } 17 | } 18 | 19 | impl Deref for MessageRawBodyArg { 20 | type Target = Vec; 21 | 22 | fn deref(&self) -> &Self::Target { 23 | &self.raw 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/email/message/arg/header.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// The envelope id argument parser. 4 | #[derive(Debug, Parser)] 5 | pub struct HeaderRawArgs { 6 | /// Prefill the template with custom headers. 7 | /// 8 | /// A raw header should follow the pattern KEY:VAL. 9 | #[arg(long = "header", short = 'H', required = false)] 10 | #[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)] 11 | pub raw: Vec<(String, String)>, 12 | } 13 | 14 | pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> { 15 | if let Some((key, val)) = raw_header.split_once(':') { 16 | Ok((key.trim().to_owned(), val.trim().to_owned())) 17 | } else { 18 | Err(format!("cannot parse raw header {raw_header:?}")) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/email/message/arg/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | pub mod body; 4 | pub mod header; 5 | pub mod reply; 6 | 7 | /// The raw message argument parser. 8 | #[derive(Debug, Parser)] 9 | pub struct MessageRawArg { 10 | /// The raw message, including headers and body. 11 | #[arg(trailing_var_arg = true)] 12 | #[arg(name = "message_raw", value_name = "MESSAGE")] 13 | pub raw: Vec, 14 | } 15 | 16 | impl MessageRawArg { 17 | pub fn raw(self) -> String { 18 | self.raw.join(" ").replace('\r', "").replace('\n', "\r\n") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/email/message/arg/reply.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | /// The reply to all argument parser. 4 | #[derive(Debug, Parser)] 5 | pub struct MessageReplyAllArg { 6 | /// Reply to all recipients. 7 | /// 8 | /// This argument will add all recipients for the To and Cc 9 | /// headers. 10 | #[arg(long, short = 'A')] 11 | pub all: bool, 12 | } 13 | -------------------------------------------------------------------------------- /src/email/message/attachment/command/download.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::{eyre::Context, Result}; 3 | use email::{backend::feature::BackendFeatureSource, config::Config}; 4 | use pimalaya_tui::{ 5 | himalaya::backend::BackendBuilder, 6 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 7 | }; 8 | use std::{fs, path::PathBuf, sync::Arc}; 9 | use tracing::info; 10 | use uuid::Uuid; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, 14 | folder::arg::name::FolderNameOptionalFlag, 15 | }; 16 | 17 | /// Download all attachments found in the given message. 18 | /// 19 | /// This command allows you to download all attachments found for the 20 | /// given message to your downloads directory. 21 | #[derive(Debug, Parser)] 22 | pub struct AttachmentDownloadCommand { 23 | #[command(flatten)] 24 | pub folder: FolderNameOptionalFlag, 25 | 26 | #[command(flatten)] 27 | pub envelopes: EnvelopeIdsArgs, 28 | 29 | #[command(flatten)] 30 | pub account: AccountNameFlag, 31 | } 32 | 33 | impl AttachmentDownloadCommand { 34 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 35 | info!("executing download attachment(s) command"); 36 | 37 | let folder = &self.folder.name; 38 | let ids = &self.envelopes.ids; 39 | 40 | let (toml_account_config, account_config) = config 41 | .clone() 42 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 43 | c.account(name).ok() 44 | })?; 45 | 46 | let account_config = Arc::new(account_config); 47 | 48 | let backend = BackendBuilder::new( 49 | Arc::new(toml_account_config), 50 | account_config.clone(), 51 | |builder| { 52 | builder 53 | .without_features() 54 | .with_get_messages(BackendFeatureSource::Context) 55 | }, 56 | ) 57 | .without_sending_backend() 58 | .build() 59 | .await?; 60 | 61 | let emails = backend.get_messages(folder, ids).await?; 62 | 63 | let mut emails_count = 0; 64 | let mut attachments_count = 0; 65 | 66 | let mut ids = ids.iter(); 67 | for email in emails.to_vec() { 68 | let id = ids.next().unwrap(); 69 | let attachments = email.attachments()?; 70 | 71 | if attachments.is_empty() { 72 | printer.log(format!("No attachment found for message {id}!\n"))?; 73 | continue; 74 | } else { 75 | emails_count += 1; 76 | } 77 | 78 | printer.log(format!( 79 | "{} attachment(s) found for message {id}!\n", 80 | attachments.len() 81 | ))?; 82 | 83 | for attachment in attachments { 84 | let filename: PathBuf = attachment 85 | .filename 86 | .unwrap_or_else(|| Uuid::new_v4().to_string()) 87 | .into(); 88 | let filepath = account_config.get_download_file_path(&filename)?; 89 | printer.log(format!("Downloading {:?}…\n", filepath))?; 90 | fs::write(&filepath, &attachment.body) 91 | .with_context(|| format!("cannot save attachment at {filepath:?}"))?; 92 | attachments_count += 1; 93 | } 94 | } 95 | 96 | match attachments_count { 97 | 0 => printer.out("No attachment found!\n"), 98 | 1 => printer.out("Downloaded 1 attachment!\n"), 99 | n => printer.out(format!( 100 | "Downloaded {} attachment(s) from {} messages(s)!\n", 101 | n, emails_count, 102 | )), 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/email/message/attachment/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod download; 2 | 3 | use clap::Subcommand; 4 | use color_eyre::Result; 5 | use pimalaya_tui::terminal::cli::printer::Printer; 6 | 7 | use crate::config::TomlConfig; 8 | 9 | use self::download::AttachmentDownloadCommand; 10 | 11 | /// Download your message attachments. 12 | /// 13 | /// A message body can be composed of multiple MIME parts. An 14 | /// attachment is the representation of a binary part of a message 15 | /// body. 16 | #[derive(Debug, Subcommand)] 17 | pub enum AttachmentSubcommand { 18 | #[command(arg_required_else_help = true, alias = "dl")] 19 | Download(AttachmentDownloadCommand), 20 | } 21 | 22 | impl AttachmentSubcommand { 23 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 24 | match self { 25 | Self::Download(cmd) => cmd.execute(printer, config).await, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/email/message/attachment/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | -------------------------------------------------------------------------------- /src/email/message/command/copy.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | envelope::arg::ids::EnvelopeIdsArgs, 16 | folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, 17 | }; 18 | 19 | /// Copy the message associated to the given envelope id(s) to the 20 | /// given target folder. 21 | #[derive(Debug, Parser)] 22 | pub struct MessageCopyCommand { 23 | #[command(flatten)] 24 | pub source_folder: SourceFolderNameOptionalFlag, 25 | 26 | #[command(flatten)] 27 | pub target_folder: TargetFolderNameArg, 28 | 29 | #[command(flatten)] 30 | pub envelopes: EnvelopeIdsArgs, 31 | 32 | #[command(flatten)] 33 | pub account: AccountNameFlag, 34 | } 35 | 36 | impl MessageCopyCommand { 37 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 38 | info!("executing copy message(s) command"); 39 | 40 | let source = &self.source_folder.name; 41 | let target = &self.target_folder.name; 42 | let ids = &self.envelopes.ids; 43 | 44 | let (toml_account_config, account_config) = config 45 | .clone() 46 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 47 | c.account(name).ok() 48 | })?; 49 | 50 | let backend = BackendBuilder::new( 51 | Arc::new(toml_account_config), 52 | Arc::new(account_config), 53 | |builder| { 54 | builder 55 | .without_features() 56 | .with_copy_messages(BackendFeatureSource::Context) 57 | }, 58 | ) 59 | .without_sending_backend() 60 | .build() 61 | .await?; 62 | 63 | backend.copy_messages(source, target, ids).await?; 64 | 65 | printer.out(format!( 66 | "Message(s) successfully copied from {source} to {target}!\n" 67 | )) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/email/message/command/delete.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, 14 | folder::arg::name::FolderNameOptionalFlag, 15 | }; 16 | 17 | /// Mark as deleted the message associated to the given envelope id(s). 18 | /// 19 | /// This command does not really delete the message: if the given 20 | /// folder points to the trash folder, it adds the "deleted" flag to 21 | /// its envelope, otherwise it moves it to the trash folder. Only the 22 | /// expunge folder command truly deletes messages. 23 | #[derive(Debug, Parser)] 24 | pub struct MessageDeleteCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameOptionalFlag, 27 | 28 | #[command(flatten)] 29 | pub envelopes: EnvelopeIdsArgs, 30 | 31 | #[command(flatten)] 32 | pub account: AccountNameFlag, 33 | } 34 | 35 | impl MessageDeleteCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing delete message(s) command"); 38 | 39 | let folder = &self.folder.name; 40 | let ids = &self.envelopes.ids; 41 | 42 | let (toml_account_config, account_config) = config 43 | .clone() 44 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 45 | c.account(name).ok() 46 | })?; 47 | 48 | let backend = BackendBuilder::new( 49 | Arc::new(toml_account_config), 50 | Arc::new(account_config), 51 | |builder| { 52 | builder 53 | .without_features() 54 | .with_delete_messages(BackendFeatureSource::Context) 55 | }, 56 | ) 57 | .without_sending_backend() 58 | .build() 59 | .await?; 60 | 61 | backend.delete_messages(folder, ids).await?; 62 | 63 | printer.out(format!("Message(s) successfully removed from {folder}!\n")) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/email/message/command/edit.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::{eyre::eyre, Result}; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::{backend::BackendBuilder, editor}, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, 14 | folder::arg::name::FolderNameOptionalFlag, 15 | }; 16 | 17 | /// Edit the message associated to the given envelope id. 18 | /// 19 | /// This command allows you to edit the given message using the 20 | /// editor defined in your environment variable $EDITOR. When the 21 | /// edition process finishes, you can choose between saving or sending 22 | /// the final message. 23 | #[derive(Debug, Parser)] 24 | pub struct MessageEditCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameOptionalFlag, 27 | 28 | #[command(flatten)] 29 | pub envelope: EnvelopeIdArg, 30 | 31 | /// List of headers that should be visible at the top of the 32 | /// message. 33 | /// 34 | /// If a given header is not found in the message, it will not be 35 | /// visible. If no header is given, defaults to the one set up in 36 | /// your TOML configuration file. 37 | #[arg(long = "header", short = 'H', value_name = "NAME")] 38 | pub headers: Vec, 39 | 40 | /// Edit the message on place. 41 | /// 42 | /// If set, the original message being edited will be removed at 43 | /// the end of the command. Useful when you need, for example, to 44 | /// edit a draft, send it then remove it from the Drafts folder. 45 | #[arg(long, short = 'p')] 46 | pub on_place: bool, 47 | 48 | #[command(flatten)] 49 | pub account: AccountNameFlag, 50 | } 51 | 52 | impl MessageEditCommand { 53 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 54 | info!("executing edit message command"); 55 | 56 | let folder = &self.folder.name; 57 | 58 | let (toml_account_config, account_config) = config 59 | .clone() 60 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 61 | c.account(name).ok() 62 | })?; 63 | 64 | let account_config = Arc::new(account_config); 65 | 66 | let backend = BackendBuilder::new( 67 | Arc::new(toml_account_config), 68 | account_config.clone(), 69 | |builder| { 70 | builder 71 | .without_features() 72 | .with_add_message(BackendFeatureSource::Context) 73 | .with_send_message(BackendFeatureSource::Context) 74 | .with_delete_messages(BackendFeatureSource::Context) 75 | }, 76 | ) 77 | .build() 78 | .await?; 79 | 80 | let id = self.envelope.id; 81 | let tpl = backend 82 | .get_messages(folder, &[id]) 83 | .await? 84 | .first() 85 | .ok_or(eyre!("cannot find message"))? 86 | .to_read_tpl(&account_config, |mut tpl| { 87 | if !self.headers.is_empty() { 88 | tpl = tpl.with_show_only_headers(&self.headers); 89 | } 90 | 91 | tpl 92 | }) 93 | .await?; 94 | 95 | editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?; 96 | 97 | if self.on_place { 98 | backend.delete_messages(folder, &[id]).await?; 99 | } 100 | 101 | Ok(()) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/email/message/command/export.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | env::temp_dir, 3 | fs, 4 | io::{stdout, Write}, 5 | path::PathBuf, 6 | sync::Arc, 7 | }; 8 | 9 | use clap::Parser; 10 | use color_eyre::{eyre::eyre, Result}; 11 | use email::{backend::feature::BackendFeatureSource, config::Config}; 12 | use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _}; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg, 17 | folder::arg::name::FolderNameOptionalFlag, 18 | }; 19 | 20 | /// Export the message associated to the given envelope id. 21 | /// 22 | /// This command allows you to export a message. A message can be 23 | /// fully exported in one single file, or exported in multiple files 24 | /// (one per MIME part found in the message). This is useful, for 25 | /// example, to read a HTML message. 26 | #[derive(Debug, Parser)] 27 | pub struct MessageExportCommand { 28 | #[command(flatten)] 29 | pub folder: FolderNameOptionalFlag, 30 | 31 | #[command(flatten)] 32 | pub envelope: EnvelopeIdArg, 33 | 34 | /// Export the full raw message as one unique .eml file. 35 | /// 36 | /// The raw message represents the headers and the body as it is 37 | /// on the backend, unedited: not decoded nor decrypted. This is 38 | /// useful for debugging faulty messages, but also for 39 | /// saving/sending/transfering messages. 40 | #[arg(long, short = 'F')] 41 | pub full: bool, 42 | 43 | /// Try to open the exported message, when applicable. 44 | /// 45 | /// This argument only works with full message export, or when 46 | /// HTML or plain text is present in the export. 47 | #[arg(long, short = 'O')] 48 | pub open: bool, 49 | 50 | /// Where the message should be exported to. 51 | /// 52 | /// The destination should point to a valid directory. If `--full` 53 | /// is given, it can also point to a .eml file. 54 | #[arg(long, short, alias = "dest")] 55 | pub destination: Option, 56 | 57 | #[command(flatten)] 58 | pub account: AccountNameFlag, 59 | } 60 | 61 | impl MessageExportCommand { 62 | pub async fn execute(self, config: &TomlConfig) -> Result<()> { 63 | info!("executing export message command"); 64 | 65 | let folder = &self.folder.name; 66 | let id = &self.envelope.id; 67 | 68 | let (toml_account_config, account_config) = config 69 | .clone() 70 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 71 | c.account(name).ok() 72 | })?; 73 | 74 | let account_config = Arc::new(account_config); 75 | 76 | let backend = BackendBuilder::new( 77 | Arc::new(toml_account_config), 78 | account_config.clone(), 79 | |builder| { 80 | builder 81 | .without_features() 82 | .with_get_messages(BackendFeatureSource::Context) 83 | }, 84 | ) 85 | .without_sending_backend() 86 | .build() 87 | .await?; 88 | 89 | let msgs = backend.get_messages(folder, &[*id]).await?; 90 | let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?; 91 | 92 | if self.full { 93 | let bytes = msg.raw()?; 94 | 95 | match self.destination { 96 | Some(mut dest) if dest.is_dir() => { 97 | dest.push(format!("{id}.eml")); 98 | fs::write(&dest, bytes)?; 99 | let dest = dest.display(); 100 | println!("Message {id} successfully exported at {dest}!"); 101 | } 102 | Some(dest) => { 103 | fs::write(&dest, bytes)?; 104 | let dest = dest.display(); 105 | println!("Message {id} successfully exported at {dest}!"); 106 | } 107 | None => { 108 | stdout().write_all(bytes)?; 109 | } 110 | }; 111 | } else { 112 | let dest = match self.destination { 113 | Some(dest) if dest.is_dir() => { 114 | let dest = msg.download_parts(&dest)?; 115 | let d = dest.display(); 116 | println!("Message {id} successfully exported in {d}!"); 117 | dest 118 | } 119 | Some(dest) if dest.is_file() => { 120 | let dest = dest.parent().unwrap_or(&dest); 121 | let dest = msg.download_parts(&dest)?; 122 | let d = dest.display(); 123 | println!("Message {id} successfully exported in {d}!"); 124 | dest 125 | } 126 | Some(dest) => { 127 | return Err(eyre!("Destination {} does not exist!", dest.display())); 128 | } 129 | None => { 130 | let dest = temp_dir(); 131 | let dest = msg.download_parts(&dest)?; 132 | let d = dest.display(); 133 | println!("Message {id} successfully exported in {d}!"); 134 | dest 135 | } 136 | }; 137 | 138 | if self.open { 139 | let index_html = dest.join("index.html"); 140 | if index_html.exists() { 141 | return Ok(open::that(index_html)?); 142 | } 143 | 144 | let plain_txt = dest.join("plain.txt"); 145 | if plain_txt.exists() { 146 | return Ok(open::that(plain_txt)?); 147 | } 148 | 149 | println!("--open was passed but nothing to open, ignoring"); 150 | } 151 | } 152 | 153 | Ok(()) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/email/message/command/forward.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::{eyre::eyre, Result}; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::{backend::BackendBuilder, editor}, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | envelope::arg::ids::EnvelopeIdArg, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, 18 | }; 19 | 20 | /// Forward the message associated to the given envelope id. 21 | /// 22 | /// This command allows you to forward the given message using the 23 | /// editor defined in your environment variable $EDITOR. When the 24 | /// edition process finishes, you can choose between saving or sending 25 | /// the final message. 26 | #[derive(Debug, Parser)] 27 | pub struct MessageForwardCommand { 28 | #[command(flatten)] 29 | pub folder: FolderNameOptionalFlag, 30 | 31 | #[command(flatten)] 32 | pub envelope: EnvelopeIdArg, 33 | 34 | #[command(flatten)] 35 | pub headers: HeaderRawArgs, 36 | 37 | #[command(flatten)] 38 | pub body: MessageRawBodyArg, 39 | 40 | #[command(flatten)] 41 | pub account: AccountNameFlag, 42 | } 43 | 44 | impl MessageForwardCommand { 45 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 46 | info!("executing forward message command"); 47 | 48 | let folder = &self.folder.name; 49 | 50 | let (toml_account_config, account_config) = config 51 | .clone() 52 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 53 | c.account(name).ok() 54 | })?; 55 | 56 | let account_config = Arc::new(account_config); 57 | 58 | let backend = BackendBuilder::new( 59 | Arc::new(toml_account_config), 60 | account_config.clone(), 61 | |builder| { 62 | builder 63 | .without_features() 64 | .with_add_message(BackendFeatureSource::Context) 65 | .with_send_message(BackendFeatureSource::Context) 66 | }, 67 | ) 68 | .build() 69 | .await?; 70 | 71 | let id = self.envelope.id; 72 | let tpl = backend 73 | .get_messages(folder, &[id]) 74 | .await? 75 | .first() 76 | .ok_or(eyre!("cannot find message"))? 77 | .to_forward_tpl_builder(account_config.clone()) 78 | .with_headers(self.headers.raw) 79 | .with_body(self.body.raw()) 80 | .build() 81 | .await?; 82 | editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/email/message/command/mailto.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::{backend::BackendBuilder, editor}, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | use url::Url; 12 | 13 | use crate::{account::arg::name::AccountNameFlag, config::TomlConfig}; 14 | 15 | /// Parse and edit a message from the given mailto URL string. 16 | /// 17 | /// This command allows you to edit a message from the mailto format 18 | /// using the editor defined in your environment variable 19 | /// $EDITOR. When the edition process finishes, you can choose between 20 | /// saving or sending the final message. 21 | #[derive(Debug, Parser)] 22 | pub struct MessageMailtoCommand { 23 | /// The mailto url. 24 | #[arg()] 25 | pub url: Url, 26 | 27 | #[command(flatten)] 28 | pub account: AccountNameFlag, 29 | } 30 | 31 | impl MessageMailtoCommand { 32 | pub fn new(url: &str) -> Result { 33 | Ok(Self { 34 | url: Url::parse(url)?, 35 | account: Default::default(), 36 | }) 37 | } 38 | 39 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 40 | info!("executing mailto message command"); 41 | 42 | let (toml_account_config, account_config) = config 43 | .clone() 44 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 45 | c.account(name).ok() 46 | })?; 47 | 48 | let account_config = Arc::new(account_config); 49 | 50 | let backend = BackendBuilder::new( 51 | Arc::new(toml_account_config), 52 | account_config.clone(), 53 | |builder| { 54 | builder 55 | .without_features() 56 | .with_add_message(BackendFeatureSource::Context) 57 | .with_send_message(BackendFeatureSource::Context) 58 | }, 59 | ) 60 | .without_sending_backend() 61 | .build() 62 | .await?; 63 | 64 | let mut msg = Vec::::new(); 65 | let mut body = Vec::::new(); 66 | 67 | msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n"); 68 | 69 | for (key, val) in self.url.query_pairs() { 70 | if key.eq_ignore_ascii_case("body") { 71 | body.extend(val.as_bytes()); 72 | } else { 73 | msg.extend(key.as_bytes()); 74 | msg.extend(b": "); 75 | msg.extend(val.as_bytes()); 76 | msg.extend(b"\r\n"); 77 | } 78 | } 79 | 80 | msg.extend(b"\r\n"); 81 | msg.extend(body); 82 | 83 | if let Some(sig) = account_config.find_full_signature() { 84 | msg.extend(b"\r\n"); 85 | msg.extend(sig.as_bytes()); 86 | } 87 | 88 | let tpl = account_config 89 | .generate_tpl_interpreter() 90 | .with_show_only_headers(account_config.get_message_write_headers()) 91 | .build() 92 | .from_bytes(msg) 93 | .await? 94 | .into(); 95 | 96 | editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/email/message/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod copy; 2 | pub mod delete; 3 | pub mod edit; 4 | pub mod export; 5 | pub mod forward; 6 | pub mod mailto; 7 | pub mod r#move; 8 | pub mod read; 9 | pub mod reply; 10 | pub mod save; 11 | pub mod send; 12 | pub mod thread; 13 | pub mod write; 14 | 15 | use clap::Subcommand; 16 | use color_eyre::Result; 17 | use pimalaya_tui::terminal::cli::printer::Printer; 18 | 19 | use crate::config::TomlConfig; 20 | 21 | use self::{ 22 | copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand, 23 | export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand, 24 | r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand, 25 | save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand, 26 | write::MessageWriteCommand, 27 | }; 28 | 29 | /// Read, write, send, copy, move and delete your messages. 30 | /// 31 | /// A message is the content of an email. It is composed of headers 32 | /// (located at the top of the message) and a body (located at the 33 | /// bottom of the message). Both are separated by two new lines. This 34 | /// subcommand allows you to manage them. 35 | #[derive(Debug, Subcommand)] 36 | pub enum MessageSubcommand { 37 | #[command(arg_required_else_help = true)] 38 | Read(MessageReadCommand), 39 | 40 | #[command(arg_required_else_help = true)] 41 | Export(MessageExportCommand), 42 | 43 | #[command(arg_required_else_help = true)] 44 | Thread(MessageThreadCommand), 45 | 46 | #[command(aliases = ["add", "create", "new", "compose"])] 47 | Write(MessageWriteCommand), 48 | 49 | Reply(MessageReplyCommand), 50 | 51 | #[command(aliases = ["fwd", "fd"])] 52 | Forward(MessageForwardCommand), 53 | 54 | Edit(MessageEditCommand), 55 | 56 | Mailto(MessageMailtoCommand), 57 | 58 | Save(MessageSaveCommand), 59 | 60 | Send(MessageSendCommand), 61 | 62 | #[command(arg_required_else_help = true)] 63 | #[command(aliases = ["cpy", "cp"])] 64 | Copy(MessageCopyCommand), 65 | 66 | #[command(arg_required_else_help = true)] 67 | #[command(alias = "mv")] 68 | Move(MessageMoveCommand), 69 | 70 | #[command(arg_required_else_help = true)] 71 | #[command(aliases = ["remove", "rm"])] 72 | Delete(MessageDeleteCommand), 73 | } 74 | 75 | impl MessageSubcommand { 76 | #[allow(unused)] 77 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 78 | match self { 79 | Self::Read(cmd) => cmd.execute(printer, config).await, 80 | Self::Export(cmd) => cmd.execute(config).await, 81 | Self::Thread(cmd) => cmd.execute(printer, config).await, 82 | Self::Write(cmd) => cmd.execute(printer, config).await, 83 | Self::Reply(cmd) => cmd.execute(printer, config).await, 84 | Self::Forward(cmd) => cmd.execute(printer, config).await, 85 | Self::Edit(cmd) => cmd.execute(printer, config).await, 86 | Self::Mailto(cmd) => cmd.execute(printer, config).await, 87 | Self::Save(cmd) => cmd.execute(printer, config).await, 88 | Self::Send(cmd) => cmd.execute(printer, config).await, 89 | Self::Copy(cmd) => cmd.execute(printer, config).await, 90 | Self::Move(cmd) => cmd.execute(printer, config).await, 91 | Self::Delete(cmd) => cmd.execute(printer, config).await, 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/email/message/command/move.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | #[allow(unused)] 13 | use crate::{ 14 | account::arg::name::AccountNameFlag, 15 | config::TomlConfig, 16 | envelope::arg::ids::EnvelopeIdsArgs, 17 | folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg}, 18 | }; 19 | 20 | /// Move the message associated to the given envelope id(s) to the 21 | /// given target folder. 22 | #[derive(Debug, Parser)] 23 | pub struct MessageMoveCommand { 24 | #[command(flatten)] 25 | pub source_folder: SourceFolderNameOptionalFlag, 26 | 27 | #[command(flatten)] 28 | pub target_folder: TargetFolderNameArg, 29 | 30 | #[command(flatten)] 31 | pub envelopes: EnvelopeIdsArgs, 32 | 33 | #[command(flatten)] 34 | pub account: AccountNameFlag, 35 | } 36 | 37 | impl MessageMoveCommand { 38 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 39 | info!("executing move message(s) command"); 40 | 41 | let source = &self.source_folder.name; 42 | let target = &self.target_folder.name; 43 | let ids = &self.envelopes.ids; 44 | 45 | let (toml_account_config, account_config) = config 46 | .clone() 47 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 48 | c.account(name).ok() 49 | })?; 50 | 51 | let backend = BackendBuilder::new( 52 | Arc::new(toml_account_config), 53 | Arc::new(account_config), 54 | |builder| { 55 | builder 56 | .without_features() 57 | .with_move_messages(BackendFeatureSource::Context) 58 | }, 59 | ) 60 | .without_sending_backend() 61 | .build() 62 | .await?; 63 | 64 | backend.move_messages(source, target, ids).await?; 65 | 66 | printer.out(format!( 67 | "Message(s) successfully moved from {source} to {target}!\n" 68 | )) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/email/message/command/read.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | #[allow(unused)] 13 | use crate::{ 14 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, 15 | folder::arg::name::FolderNameOptionalFlag, 16 | }; 17 | 18 | /// Read a human-friendly version of the message associated to the 19 | /// given envelope id(s). 20 | /// 21 | /// This command allows you to read a message. When reading a message, 22 | /// the "seen" flag is automatically applied to the corresponding 23 | /// envelope. To prevent this behaviour, use the "--preview" flag. 24 | #[derive(Debug, Parser)] 25 | pub struct MessageReadCommand { 26 | #[command(flatten)] 27 | pub folder: FolderNameOptionalFlag, 28 | 29 | #[command(flatten)] 30 | pub envelopes: EnvelopeIdsArgs, 31 | 32 | /// Read the message without applying the "seen" flag to its 33 | /// corresponding envelope. 34 | #[arg(long, short)] 35 | pub preview: bool, 36 | 37 | /// Read only the body of the message. 38 | /// 39 | /// All headers will be removed from the message. 40 | #[arg(long)] 41 | #[arg(conflicts_with = "headers")] 42 | pub no_headers: bool, 43 | 44 | /// List of headers that should be visible at the top of the 45 | /// message. 46 | /// 47 | /// If a given header is not found in the message, it will not be 48 | /// visible. If no header is given, defaults to the one set up in 49 | /// your TOML configuration file. 50 | #[arg(long = "header", short = 'H', value_name = "NAME")] 51 | #[arg(conflicts_with = "no_headers")] 52 | pub headers: Vec, 53 | 54 | #[command(flatten)] 55 | pub account: AccountNameFlag, 56 | } 57 | 58 | impl MessageReadCommand { 59 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 60 | info!("executing read message(s) command"); 61 | 62 | let folder = &self.folder.name; 63 | let ids = &self.envelopes.ids; 64 | 65 | let (toml_account_config, account_config) = config 66 | .clone() 67 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 68 | c.account(name).ok() 69 | })?; 70 | 71 | let account_config = Arc::new(account_config); 72 | 73 | let backend = BackendBuilder::new( 74 | Arc::new(toml_account_config), 75 | account_config.clone(), 76 | |builder| { 77 | builder 78 | .without_features() 79 | .with_get_messages(BackendFeatureSource::Context) 80 | .with_peek_messages(BackendFeatureSource::Context) 81 | }, 82 | ) 83 | .without_sending_backend() 84 | .build() 85 | .await?; 86 | 87 | let emails = if self.preview { 88 | backend.peek_messages(folder, ids).await 89 | } else { 90 | backend.get_messages(folder, ids).await 91 | }?; 92 | 93 | let mut glue = ""; 94 | let mut bodies = String::default(); 95 | 96 | for email in emails.to_vec() { 97 | bodies.push_str(glue); 98 | 99 | let tpl = email 100 | .to_read_tpl(&account_config, |mut tpl| { 101 | if self.no_headers { 102 | tpl = tpl.with_hide_all_headers(); 103 | } else if !self.headers.is_empty() { 104 | tpl = tpl.with_show_only_headers(&self.headers); 105 | } 106 | 107 | tpl 108 | }) 109 | .await?; 110 | bodies.push_str(&tpl); 111 | 112 | glue = "\n\n"; 113 | } 114 | 115 | printer.out(bodies) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/email/message/command/reply.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::{eyre::eyre, Result}; 5 | use email::{backend::feature::BackendFeatureSource, config::Config, flag::Flag}; 6 | use pimalaya_tui::{ 7 | himalaya::{backend::BackendBuilder, editor}, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | envelope::arg::ids::EnvelopeIdArg, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, 18 | }; 19 | 20 | /// Reply to the message associated to the given envelope id. 21 | /// 22 | /// This command allows you to reply to the given message using the 23 | /// editor defined in your environment variable $EDITOR. When the 24 | /// edition process finishes, you can choose between saving or sending 25 | /// the final message. 26 | #[derive(Debug, Parser)] 27 | pub struct MessageReplyCommand { 28 | #[command(flatten)] 29 | pub folder: FolderNameOptionalFlag, 30 | 31 | #[command(flatten)] 32 | pub envelope: EnvelopeIdArg, 33 | 34 | #[command(flatten)] 35 | pub reply: MessageReplyAllArg, 36 | 37 | #[command(flatten)] 38 | pub headers: HeaderRawArgs, 39 | 40 | #[command(flatten)] 41 | pub body: MessageRawBodyArg, 42 | 43 | #[command(flatten)] 44 | pub account: AccountNameFlag, 45 | } 46 | 47 | impl MessageReplyCommand { 48 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 49 | info!("executing reply message command"); 50 | 51 | let folder = &self.folder.name; 52 | let (toml_account_config, account_config) = config 53 | .clone() 54 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 55 | c.account(name).ok() 56 | })?; 57 | 58 | let account_config = Arc::new(account_config); 59 | 60 | let backend = BackendBuilder::new( 61 | Arc::new(toml_account_config), 62 | account_config.clone(), 63 | |builder| { 64 | builder 65 | .without_features() 66 | .with_add_message(BackendFeatureSource::Context) 67 | .with_send_message(BackendFeatureSource::Context) 68 | }, 69 | ) 70 | .build() 71 | .await?; 72 | 73 | let id = self.envelope.id; 74 | let tpl = backend 75 | .get_messages(folder, &[id]) 76 | .await? 77 | .first() 78 | .ok_or(eyre!("cannot find message {id}"))? 79 | .to_reply_tpl_builder(account_config.clone()) 80 | .with_headers(self.headers.raw) 81 | .with_body(self.body.raw()) 82 | .with_reply_all(self.reply.all) 83 | .build() 84 | .await?; 85 | 86 | editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?; 87 | 88 | backend.add_flag(folder, &[id], Flag::Answered).await?; 89 | 90 | Ok(()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/email/message/command/save.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use email::{backend::feature::BackendFeatureSource, config::Config}; 4 | use pimalaya_tui::{ 5 | himalaya::backend::BackendBuilder, 6 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 7 | }; 8 | use std::{ 9 | io::{self, BufRead, IsTerminal}, 10 | sync::Arc, 11 | }; 12 | use tracing::info; 13 | 14 | use crate::{ 15 | account::arg::name::AccountNameFlag, config::TomlConfig, 16 | folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, 17 | }; 18 | 19 | /// Save the given raw message to the given folder. 20 | /// 21 | /// This command allows you to add a raw message to the given folder. 22 | #[derive(Debug, Parser)] 23 | pub struct MessageSaveCommand { 24 | #[command(flatten)] 25 | pub folder: FolderNameOptionalFlag, 26 | 27 | #[command(flatten)] 28 | pub message: MessageRawArg, 29 | 30 | #[command(flatten)] 31 | pub account: AccountNameFlag, 32 | } 33 | 34 | impl MessageSaveCommand { 35 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 36 | info!("executing save message command"); 37 | 38 | let folder = &self.folder.name; 39 | 40 | let (toml_account_config, account_config) = config 41 | .clone() 42 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 43 | c.account(name).ok() 44 | })?; 45 | 46 | let backend = BackendBuilder::new( 47 | Arc::new(toml_account_config), 48 | Arc::new(account_config), 49 | |builder| { 50 | builder 51 | .without_features() 52 | .with_add_message(BackendFeatureSource::Context) 53 | }, 54 | ) 55 | .without_sending_backend() 56 | .build() 57 | .await?; 58 | 59 | let is_tty = io::stdin().is_terminal(); 60 | let is_json = printer.is_json(); 61 | let msg = if is_tty || is_json { 62 | self.message.raw() 63 | } else { 64 | io::stdin() 65 | .lock() 66 | .lines() 67 | .map_while(Result::ok) 68 | .collect::>() 69 | .join("\r\n") 70 | }; 71 | 72 | backend.add_message(folder, msg.as_bytes()).await?; 73 | 74 | printer.out(format!("Message successfully saved to {folder}!\n")) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/email/message/command/send.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use email::{backend::feature::BackendFeatureSource, config::Config}; 4 | use pimalaya_tui::{ 5 | himalaya::backend::BackendBuilder, 6 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 7 | }; 8 | use std::{ 9 | io::{self, BufRead, IsTerminal}, 10 | sync::Arc, 11 | }; 12 | use tracing::info; 13 | 14 | use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg}; 15 | 16 | /// Send the given raw message. 17 | /// 18 | /// This command allows you to send a raw message and to save a copy 19 | /// to your send folder. 20 | #[derive(Debug, Parser)] 21 | pub struct MessageSendCommand { 22 | #[command(flatten)] 23 | pub message: MessageRawArg, 24 | 25 | #[command(flatten)] 26 | pub account: AccountNameFlag, 27 | } 28 | 29 | impl MessageSendCommand { 30 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 31 | info!("executing send message command"); 32 | 33 | let (toml_account_config, account_config) = config 34 | .clone() 35 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 36 | c.account(name).ok() 37 | })?; 38 | 39 | let backend = BackendBuilder::new( 40 | Arc::new(toml_account_config), 41 | Arc::new(account_config), 42 | |builder| { 43 | builder 44 | .without_features() 45 | .with_add_message(BackendFeatureSource::Context) 46 | .with_send_message(BackendFeatureSource::Context) 47 | }, 48 | ) 49 | .build() 50 | .await?; 51 | 52 | let msg = if io::stdin().is_terminal() { 53 | self.message.raw() 54 | } else { 55 | io::stdin() 56 | .lock() 57 | .lines() 58 | .map_while(Result::ok) 59 | .collect::>() 60 | .join("\r\n") 61 | }; 62 | 63 | backend.send_message_then_save_copy(msg.as_bytes()).await?; 64 | 65 | printer.out("Message successfully sent!") 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/email/message/command/thread.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::envelope::arg::ids::EnvelopeIdArg; 13 | #[allow(unused)] 14 | use crate::{ 15 | account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | }; 18 | 19 | /// Read human-friendly version of messages associated to the 20 | /// given envelope id's thread. 21 | /// 22 | /// This command allows you to thread a message. When threading a message, 23 | /// the "seen" flag is automatically applied to the corresponding 24 | /// envelope. To prevent this behaviour, use the --preview flag. 25 | #[derive(Debug, Parser)] 26 | pub struct MessageThreadCommand { 27 | #[command(flatten)] 28 | pub folder: FolderNameOptionalFlag, 29 | 30 | #[command(flatten)] 31 | pub envelope: EnvelopeIdArg, 32 | 33 | /// Thread the message without applying the "seen" flag to its 34 | /// corresponding envelope. 35 | #[arg(long, short)] 36 | pub preview: bool, 37 | 38 | /// Thread only the body of the message. 39 | /// 40 | /// All headers will be removed from the message. 41 | #[arg(long)] 42 | #[arg(conflicts_with = "headers")] 43 | pub no_headers: bool, 44 | 45 | /// List of headers that should be visible at the top of the 46 | /// message. 47 | /// 48 | /// If a given header is not found in the message, it will not be 49 | /// visible. If no header is given, defaults to the one set up in 50 | /// your TOML configuration file. 51 | #[arg(long = "header", short = 'H', value_name = "NAME")] 52 | #[arg(conflicts_with = "no_headers")] 53 | pub headers: Vec, 54 | 55 | #[command(flatten)] 56 | pub account: AccountNameFlag, 57 | } 58 | 59 | impl MessageThreadCommand { 60 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 61 | info!("executing thread message(s) command"); 62 | 63 | let folder = &self.folder.name; 64 | let id = &self.envelope.id; 65 | 66 | let (toml_account_config, account_config) = config 67 | .clone() 68 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 69 | c.account(name).ok() 70 | })?; 71 | 72 | let account_config = Arc::new(account_config); 73 | 74 | let backend = BackendBuilder::new( 75 | Arc::new(toml_account_config), 76 | account_config.clone(), 77 | |builder| { 78 | builder 79 | .without_features() 80 | .with_get_messages(BackendFeatureSource::Context) 81 | .with_peek_messages(BackendFeatureSource::Context) 82 | .with_thread_envelopes(BackendFeatureSource::Context) 83 | }, 84 | ) 85 | .without_sending_backend() 86 | .build() 87 | .await?; 88 | 89 | let envelopes = backend 90 | .thread_envelope(folder, *id, Default::default()) 91 | .await?; 92 | 93 | let ids: Vec<_> = envelopes 94 | .graph() 95 | .nodes() 96 | .map(|e| e.id.parse::().unwrap()) 97 | .collect(); 98 | 99 | let emails = if self.preview { 100 | backend.peek_messages(folder, &ids).await 101 | } else { 102 | backend.get_messages(folder, &ids).await 103 | }?; 104 | 105 | let mut glue = ""; 106 | let mut bodies = String::default(); 107 | 108 | for (i, email) in emails.to_vec().iter().enumerate() { 109 | bodies.push_str(glue); 110 | bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1])); 111 | 112 | let tpl = email 113 | .to_read_tpl(&account_config, |mut tpl| { 114 | if self.no_headers { 115 | tpl = tpl.with_hide_all_headers(); 116 | } else if !self.headers.is_empty() { 117 | tpl = tpl.with_show_only_headers(&self.headers); 118 | } 119 | 120 | tpl 121 | }) 122 | .await?; 123 | 124 | bodies.push_str(&tpl); 125 | glue = "\n\n"; 126 | } 127 | 128 | printer.out(bodies) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/email/message/command/write.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{ 6 | config::Config, 7 | {backend::feature::BackendFeatureSource, message::Message}, 8 | }; 9 | use pimalaya_tui::{ 10 | himalaya::{backend::BackendBuilder, editor}, 11 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 12 | }; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, 17 | config::TomlConfig, 18 | message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, 19 | }; 20 | 21 | /// Compose a new message, from scratch. 22 | /// 23 | /// This command allows you to write a new message using the editor 24 | /// defined in your environment variable $EDITOR. When the edition 25 | /// process finishes, you can choose between saving or sending the 26 | /// final message. 27 | #[derive(Debug, Parser)] 28 | pub struct MessageWriteCommand { 29 | #[command(flatten)] 30 | pub headers: HeaderRawArgs, 31 | 32 | #[command(flatten)] 33 | pub body: MessageRawBodyArg, 34 | 35 | #[command(flatten)] 36 | pub account: AccountNameFlag, 37 | } 38 | 39 | impl MessageWriteCommand { 40 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 41 | info!("executing write message command"); 42 | 43 | let (toml_account_config, account_config) = config 44 | .clone() 45 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 46 | c.account(name).ok() 47 | })?; 48 | 49 | let account_config = Arc::new(account_config); 50 | 51 | let backend = BackendBuilder::new( 52 | Arc::new(toml_account_config), 53 | account_config.clone(), 54 | |builder| { 55 | builder 56 | .without_features() 57 | .with_add_message(BackendFeatureSource::Context) 58 | .with_send_message(BackendFeatureSource::Context) 59 | }, 60 | ) 61 | .build() 62 | .await?; 63 | 64 | let tpl = Message::new_tpl_builder(account_config.clone()) 65 | .with_headers(self.headers.raw) 66 | .with_body(self.body.raw()) 67 | .build() 68 | .await?; 69 | 70 | editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/email/message/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod attachment; 3 | pub mod command; 4 | pub mod template; 5 | -------------------------------------------------------------------------------- /src/email/message/template/arg/body.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::ops::Deref; 3 | 4 | /// The raw template body argument parser. 5 | #[derive(Debug, Parser)] 6 | pub struct TemplateRawBodyArg { 7 | /// Prefill the template with a custom MML body. 8 | #[arg(trailing_var_arg = true)] 9 | #[arg(name = "body_raw", value_name = "BODY")] 10 | pub raw: Vec, 11 | } 12 | 13 | impl TemplateRawBodyArg { 14 | pub fn raw(self) -> String { 15 | self.raw.join(" ").replace('\r', "") 16 | } 17 | } 18 | 19 | impl Deref for TemplateRawBodyArg { 20 | type Target = Vec; 21 | 22 | fn deref(&self) -> &Self::Target { 23 | &self.raw 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/email/message/template/arg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod body; 2 | 3 | use clap::Parser; 4 | 5 | /// The raw template argument parser. 6 | #[derive(Debug, Parser)] 7 | pub struct TemplateRawArg { 8 | /// The raw template, including headers and MML body. 9 | #[arg(trailing_var_arg = true)] 10 | #[arg(name = "template_raw", value_name = "TEMPLATE")] 11 | pub raw: Vec, 12 | } 13 | 14 | impl TemplateRawArg { 15 | pub fn raw(self) -> String { 16 | self.raw.join(" ").replace('\r', "") 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/email/message/template/command/forward.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::{eyre::eyre, Result}; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | envelope::arg::ids::EnvelopeIdArg, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs}, 18 | }; 19 | 20 | /// Generate a template for forwarding a message. 21 | /// 22 | /// The generated template is prefilled with your email in a From 23 | /// header as well as your signature. The forwarded message is also 24 | /// prefilled in the body of the template, prefixed by a separator. 25 | #[derive(Debug, Parser)] 26 | pub struct TemplateForwardCommand { 27 | #[command(flatten)] 28 | pub folder: FolderNameOptionalFlag, 29 | 30 | #[command(flatten)] 31 | pub envelope: EnvelopeIdArg, 32 | 33 | #[command(flatten)] 34 | pub headers: HeaderRawArgs, 35 | 36 | #[command(flatten)] 37 | pub body: MessageRawBodyArg, 38 | 39 | #[command(flatten)] 40 | pub account: AccountNameFlag, 41 | } 42 | 43 | impl TemplateForwardCommand { 44 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 45 | info!("executing forward template command"); 46 | 47 | let folder = &self.folder.name; 48 | 49 | let (toml_account_config, account_config) = config 50 | .clone() 51 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 52 | c.account(name).ok() 53 | })?; 54 | 55 | let account_config = Arc::new(account_config); 56 | 57 | let backend = BackendBuilder::new( 58 | Arc::new(toml_account_config), 59 | account_config.clone(), 60 | |builder| { 61 | builder 62 | .without_features() 63 | .with_get_messages(BackendFeatureSource::Context) 64 | }, 65 | ) 66 | .without_sending_backend() 67 | .build() 68 | .await?; 69 | 70 | let id = self.envelope.id; 71 | let tpl = backend 72 | .get_messages(folder, &[id]) 73 | .await? 74 | .first() 75 | .ok_or(eyre!("cannot find message {id}"))? 76 | .to_forward_tpl_builder(account_config) 77 | .with_headers(self.headers.raw) 78 | .with_body(self.body.raw()) 79 | .build() 80 | .await?; 81 | 82 | printer.out(tpl) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/email/message/template/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod forward; 2 | mod reply; 3 | mod save; 4 | mod send; 5 | mod write; 6 | 7 | use clap::Subcommand; 8 | use color_eyre::Result; 9 | use pimalaya_tui::terminal::cli::printer::Printer; 10 | 11 | use crate::config::TomlConfig; 12 | 13 | use self::{ 14 | forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand, 15 | send::TemplateSendCommand, write::TemplateWriteCommand, 16 | }; 17 | 18 | /// Generate, save and send message templates. 19 | /// 20 | /// A template is an editable version of a message (headers + 21 | /// body). It uses a specific language called MML that allows you to 22 | /// attach file or encrypt content. This subcommand allows you manage 23 | /// them. 24 | /// 25 | /// Learn more about MML at: . 26 | #[derive(Debug, Subcommand)] 27 | pub enum TemplateSubcommand { 28 | #[command(aliases = ["add", "create", "new", "compose"])] 29 | Write(TemplateWriteCommand), 30 | 31 | #[command(arg_required_else_help = true)] 32 | Reply(TemplateReplyCommand), 33 | 34 | #[command(arg_required_else_help = true)] 35 | #[command(alias = "fwd")] 36 | Forward(TemplateForwardCommand), 37 | 38 | #[command()] 39 | Save(TemplateSaveCommand), 40 | 41 | #[command()] 42 | Send(TemplateSendCommand), 43 | } 44 | 45 | impl TemplateSubcommand { 46 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 47 | match self { 48 | Self::Write(cmd) => cmd.execute(printer, config).await, 49 | Self::Reply(cmd) => cmd.execute(printer, config).await, 50 | Self::Forward(cmd) => cmd.execute(printer, config).await, 51 | Self::Save(cmd) => cmd.execute(printer, config).await, 52 | Self::Send(cmd) => cmd.execute(printer, config).await, 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/email/message/template/command/reply.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::{eyre::eyre, Result}; 5 | use email::{backend::feature::BackendFeatureSource, config::Config}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, 14 | config::TomlConfig, 15 | envelope::arg::ids::EnvelopeIdArg, 16 | folder::arg::name::FolderNameOptionalFlag, 17 | message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg}, 18 | }; 19 | 20 | /// Generate a template for replying to a message. 21 | /// 22 | /// The generated template is prefilled with your email in a From 23 | /// header as well as your signature. The replied message is also 24 | /// prefilled in the body of the template, with all lines prefixed by 25 | /// the symbol greater than ">". 26 | #[derive(Debug, Parser)] 27 | pub struct TemplateReplyCommand { 28 | #[command(flatten)] 29 | pub folder: FolderNameOptionalFlag, 30 | 31 | #[command(flatten)] 32 | pub envelope: EnvelopeIdArg, 33 | 34 | #[command(flatten)] 35 | pub reply: MessageReplyAllArg, 36 | 37 | #[command(flatten)] 38 | pub headers: HeaderRawArgs, 39 | 40 | #[command(flatten)] 41 | pub body: MessageRawBodyArg, 42 | 43 | #[command(flatten)] 44 | pub account: AccountNameFlag, 45 | } 46 | 47 | impl TemplateReplyCommand { 48 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 49 | info!("executing reply template command"); 50 | 51 | let folder = &self.folder.name; 52 | let id = self.envelope.id; 53 | 54 | let (toml_account_config, account_config) = config 55 | .clone() 56 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 57 | c.account(name).ok() 58 | })?; 59 | 60 | let account_config = Arc::new(account_config); 61 | 62 | let backend = BackendBuilder::new( 63 | Arc::new(toml_account_config), 64 | account_config.clone(), 65 | |builder| { 66 | builder 67 | .without_features() 68 | .with_get_messages(BackendFeatureSource::Context) 69 | }, 70 | ) 71 | .without_sending_backend() 72 | .build() 73 | .await?; 74 | 75 | let tpl = backend 76 | .get_messages(folder, &[id]) 77 | .await? 78 | .first() 79 | .ok_or(eyre!("cannot find message {id}"))? 80 | .to_reply_tpl_builder(account_config) 81 | .with_headers(self.headers.raw) 82 | .with_body(self.body.raw()) 83 | .with_reply_all(self.reply.all) 84 | .build() 85 | .await?; 86 | 87 | printer.out(tpl) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/email/message/template/command/save.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use email::{backend::feature::BackendFeatureSource, config::Config}; 4 | use mml::MmlCompilerBuilder; 5 | use pimalaya_tui::{ 6 | himalaya::backend::BackendBuilder, 7 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 8 | }; 9 | use std::{ 10 | io::{self, BufRead, IsTerminal}, 11 | sync::Arc, 12 | }; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg, 17 | folder::arg::name::FolderNameOptionalFlag, 18 | }; 19 | 20 | /// Save a template to a folder. 21 | /// 22 | /// This command allows you to save a template to the given 23 | /// folder. The template is compiled into a MIME message before being 24 | /// saved to the folder. If you want to save a raw message, use the 25 | /// message save command instead. 26 | #[derive(Debug, Parser)] 27 | pub struct TemplateSaveCommand { 28 | #[command(flatten)] 29 | pub folder: FolderNameOptionalFlag, 30 | 31 | #[command(flatten)] 32 | pub template: TemplateRawArg, 33 | 34 | #[command(flatten)] 35 | pub account: AccountNameFlag, 36 | } 37 | 38 | impl TemplateSaveCommand { 39 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 40 | info!("executing save template command"); 41 | 42 | let folder = &self.folder.name; 43 | 44 | let (toml_account_config, account_config) = config 45 | .clone() 46 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 47 | c.account(name).ok() 48 | })?; 49 | 50 | let account_config = Arc::new(account_config); 51 | 52 | let backend = BackendBuilder::new( 53 | Arc::new(toml_account_config), 54 | account_config.clone(), 55 | |builder| { 56 | builder 57 | .without_features() 58 | .with_add_message(BackendFeatureSource::Context) 59 | }, 60 | ) 61 | .without_sending_backend() 62 | .build() 63 | .await?; 64 | 65 | let is_tty = io::stdin().is_terminal(); 66 | let is_json = printer.is_json(); 67 | let tpl = if is_tty || is_json { 68 | self.template.raw() 69 | } else { 70 | io::stdin() 71 | .lock() 72 | .lines() 73 | .map_while(Result::ok) 74 | .collect::>() 75 | .join("\n") 76 | }; 77 | 78 | #[allow(unused_mut)] 79 | let mut compiler = MmlCompilerBuilder::new(); 80 | 81 | #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] 82 | compiler.set_some_pgp(account_config.pgp.clone()); 83 | 84 | let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; 85 | 86 | backend.add_message(folder, &msg).await?; 87 | 88 | printer.out(format!("Template successfully saved to {folder}!\n")) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/email/message/template/command/send.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, BufRead, IsTerminal}, 3 | sync::Arc, 4 | }; 5 | 6 | use clap::Parser; 7 | use color_eyre::Result; 8 | use email::{backend::feature::BackendFeatureSource, config::Config}; 9 | use mml::MmlCompilerBuilder; 10 | use pimalaya_tui::{ 11 | himalaya::backend::BackendBuilder, 12 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 13 | }; 14 | use tracing::info; 15 | 16 | use crate::{ 17 | account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg, 18 | }; 19 | 20 | /// Send a template. 21 | /// 22 | /// This command allows you to send a template and save a copy to the 23 | /// sent folder. The template is compiled into a MIME message before 24 | /// being sent. If you want to send a raw message, use the message 25 | /// send command instead. 26 | #[derive(Debug, Parser)] 27 | pub struct TemplateSendCommand { 28 | #[command(flatten)] 29 | pub template: TemplateRawArg, 30 | 31 | #[command(flatten)] 32 | pub account: AccountNameFlag, 33 | } 34 | 35 | impl TemplateSendCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing send template command"); 38 | 39 | let (toml_account_config, account_config) = config 40 | .clone() 41 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 42 | c.account(name).ok() 43 | })?; 44 | 45 | let account_config = Arc::new(account_config); 46 | 47 | let backend = BackendBuilder::new( 48 | Arc::new(toml_account_config), 49 | account_config.clone(), 50 | |builder| { 51 | builder 52 | .without_features() 53 | .with_add_message(BackendFeatureSource::Context) 54 | .with_send_message(BackendFeatureSource::Context) 55 | }, 56 | ) 57 | .build() 58 | .await?; 59 | 60 | let tpl = if io::stdin().is_terminal() { 61 | self.template.raw() 62 | } else { 63 | io::stdin() 64 | .lock() 65 | .lines() 66 | .map_while(Result::ok) 67 | .collect::>() 68 | .join("\n") 69 | }; 70 | 71 | #[allow(unused_mut)] 72 | let mut compiler = MmlCompilerBuilder::new(); 73 | 74 | #[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))] 75 | compiler.set_some_pgp(account_config.pgp.clone()); 76 | 77 | let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?; 78 | 79 | backend.send_message_then_save_copy(&msg).await?; 80 | 81 | printer.out("Message successfully sent!") 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/email/message/template/command/write.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{config::Config, message::Message}; 6 | use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _}; 7 | use tracing::info; 8 | 9 | use crate::{ 10 | account::arg::name::AccountNameFlag, config::TomlConfig, 11 | email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs, 12 | }; 13 | 14 | /// Generate a template for writing a new message from scratch. 15 | /// 16 | /// The generated template is prefilled with your email in a From 17 | /// header as well as your signature. 18 | #[derive(Debug, Parser)] 19 | pub struct TemplateWriteCommand { 20 | #[command(flatten)] 21 | pub headers: HeaderRawArgs, 22 | 23 | #[command(flatten)] 24 | pub body: TemplateRawBodyArg, 25 | 26 | #[command(flatten)] 27 | pub account: AccountNameFlag, 28 | } 29 | 30 | impl TemplateWriteCommand { 31 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 32 | info!("executing write template command"); 33 | 34 | let (_, account_config) = config 35 | .clone() 36 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 37 | c.account(name).ok() 38 | })?; 39 | 40 | let tpl = Message::new_tpl_builder(Arc::new(account_config)) 41 | .with_headers(self.headers.raw) 42 | .with_body(self.body.raw()) 43 | .build() 44 | .await?; 45 | 46 | printer.out(tpl) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/email/message/template/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod command; 3 | -------------------------------------------------------------------------------- /src/email/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod envelope; 2 | pub mod message; 3 | 4 | #[doc(inline)] 5 | pub use self::{ 6 | envelope::flag, 7 | message::{attachment, template}, 8 | }; 9 | -------------------------------------------------------------------------------- /src/folder/arg/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod name; 2 | -------------------------------------------------------------------------------- /src/folder/arg/name.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use email::folder::INBOX; 3 | 4 | /// The optional folder name flag parser. 5 | #[derive(Debug, Parser)] 6 | pub struct FolderNameOptionalFlag { 7 | /// The name of the folder. 8 | #[arg(long = "folder", short = 'f')] 9 | #[arg(name = "folder_name", value_name = "NAME", default_value = INBOX)] 10 | pub name: String, 11 | } 12 | 13 | impl Default for FolderNameOptionalFlag { 14 | fn default() -> Self { 15 | Self { 16 | name: INBOX.to_owned(), 17 | } 18 | } 19 | } 20 | 21 | /// The optional folder name argument parser. 22 | #[derive(Debug, Parser)] 23 | pub struct FolderNameOptionalArg { 24 | /// The name of the folder. 25 | #[arg(name = "folder_name", value_name = "FOLDER", default_value = INBOX)] 26 | pub name: String, 27 | } 28 | 29 | impl Default for FolderNameOptionalArg { 30 | fn default() -> Self { 31 | Self { 32 | name: INBOX.to_owned(), 33 | } 34 | } 35 | } 36 | 37 | /// The required folder name argument parser. 38 | #[derive(Debug, Parser)] 39 | pub struct FolderNameArg { 40 | /// The name of the folder. 41 | #[arg(name = "folder_name", value_name = "FOLDER")] 42 | pub name: String, 43 | } 44 | 45 | /// The optional source folder name flag parser. 46 | #[derive(Debug, Parser)] 47 | pub struct SourceFolderNameOptionalFlag { 48 | /// The name of the source folder. 49 | #[arg(long = "folder", short = 'f')] 50 | #[arg(name = "source_folder_name", value_name = "SOURCE", default_value = INBOX)] 51 | pub name: String, 52 | } 53 | 54 | /// The target folder name argument parser. 55 | #[derive(Debug, Parser)] 56 | pub struct TargetFolderNameArg { 57 | /// The name of the target folder. 58 | #[arg(name = "target_folder_name", value_name = "TARGET")] 59 | pub name: String, 60 | } 61 | -------------------------------------------------------------------------------- /src/folder/command/add.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{ 6 | config::Config, 7 | {backend::feature::BackendFeatureSource, folder::add::AddFolder}, 8 | }; 9 | use pimalaya_tui::{ 10 | himalaya::backend::BackendBuilder, 11 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 12 | }; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, 17 | }; 18 | 19 | /// Create the given folder. 20 | /// 21 | /// This command allows you to create a new folder using the given 22 | /// name. 23 | #[derive(Debug, Parser)] 24 | pub struct FolderAddCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameArg, 27 | 28 | #[command(flatten)] 29 | pub account: AccountNameFlag, 30 | } 31 | 32 | impl FolderAddCommand { 33 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 34 | info!("executing create folder command"); 35 | 36 | let folder = &self.folder.name; 37 | 38 | let (toml_account_config, account_config) = config 39 | .clone() 40 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 41 | c.account(name).ok() 42 | })?; 43 | 44 | let backend = BackendBuilder::new( 45 | Arc::new(toml_account_config), 46 | Arc::new(account_config), 47 | |builder| { 48 | builder 49 | .without_features() 50 | .with_add_folder(BackendFeatureSource::Context) 51 | }, 52 | ) 53 | .without_sending_backend() 54 | .build() 55 | .await?; 56 | 57 | backend.add_folder(folder).await?; 58 | 59 | printer.out(format!("Folder {folder} successfully created!\n")) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/folder/command/delete.rs: -------------------------------------------------------------------------------- 1 | use std::{process, sync::Arc}; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{ 6 | config::Config, 7 | {backend::feature::BackendFeatureSource, folder::delete::DeleteFolder}, 8 | }; 9 | use pimalaya_tui::{ 10 | himalaya::backend::BackendBuilder, 11 | terminal::{cli::printer::Printer, config::TomlConfig as _, prompt}, 12 | }; 13 | use tracing::info; 14 | 15 | use crate::{ 16 | account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, 17 | }; 18 | 19 | /// Delete the given folder. 20 | /// 21 | /// All emails from the given folder are definitely deleted. The 22 | /// folder is also deleted after execution of the command. 23 | #[derive(Debug, Parser)] 24 | pub struct FolderDeleteCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameArg, 27 | 28 | #[command(flatten)] 29 | pub account: AccountNameFlag, 30 | 31 | #[arg(long, short)] 32 | pub yes: bool, 33 | } 34 | 35 | impl FolderDeleteCommand { 36 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 37 | info!("executing delete folder command"); 38 | 39 | let folder = &self.folder.name; 40 | 41 | if !self.yes { 42 | let confirm = format!("Do you really want to delete the folder {folder}"); 43 | let confirm = format!("{confirm}? All emails will be definitely deleted."); 44 | 45 | if !prompt::bool(confirm, false)? { 46 | process::exit(0); 47 | }; 48 | } 49 | 50 | let (toml_account_config, account_config) = config 51 | .clone() 52 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 53 | c.account(name).ok() 54 | })?; 55 | 56 | let backend = BackendBuilder::new( 57 | Arc::new(toml_account_config), 58 | Arc::new(account_config), 59 | |builder| { 60 | builder 61 | .without_features() 62 | .with_delete_folder(BackendFeatureSource::Context) 63 | }, 64 | ) 65 | .without_sending_backend() 66 | .build() 67 | .await?; 68 | 69 | backend.delete_folder(folder).await?; 70 | 71 | printer.out(format!("Folder {folder} successfully deleted!\n")) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/folder/command/expunge.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{ 6 | backend::feature::BackendFeatureSource, config::Config, folder::expunge::ExpungeFolder, 7 | }; 8 | use pimalaya_tui::{ 9 | himalaya::backend::BackendBuilder, 10 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 11 | }; 12 | use tracing::info; 13 | 14 | use crate::{ 15 | account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, 16 | }; 17 | 18 | /// Expunge the given folder. 19 | /// 20 | /// The concept of expunging is similar to the IMAP one: it definitely 21 | /// deletes emails from the given folder that contain the "deleted" 22 | /// flag. 23 | #[derive(Debug, Parser)] 24 | pub struct FolderExpungeCommand { 25 | #[command(flatten)] 26 | pub folder: FolderNameArg, 27 | 28 | #[command(flatten)] 29 | pub account: AccountNameFlag, 30 | } 31 | 32 | impl FolderExpungeCommand { 33 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 34 | info!("executing expunge folder command"); 35 | 36 | let folder = &self.folder.name; 37 | let (toml_account_config, account_config) = config 38 | .clone() 39 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 40 | c.account(name).ok() 41 | })?; 42 | 43 | let backend = BackendBuilder::new( 44 | Arc::new(toml_account_config), 45 | Arc::new(account_config), 46 | |builder| { 47 | builder 48 | .without_features() 49 | .with_expunge_folder(BackendFeatureSource::Context) 50 | }, 51 | ) 52 | .without_sending_backend() 53 | .build() 54 | .await?; 55 | 56 | backend.expunge_folder(folder).await?; 57 | 58 | printer.out(format!("Folder {folder} successfully expunged!\n")) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/folder/command/list.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{ 6 | config::Config, 7 | {backend::feature::BackendFeatureSource, folder::list::ListFolders}, 8 | }; 9 | use pimalaya_tui::{ 10 | himalaya::{ 11 | backend::BackendBuilder, 12 | config::{Folders, FoldersTable}, 13 | }, 14 | terminal::{cli::printer::Printer, config::TomlConfig as _}, 15 | }; 16 | use tracing::info; 17 | 18 | use crate::{account::arg::name::AccountNameFlag, config::TomlConfig}; 19 | 20 | /// List all folders. 21 | /// 22 | /// This command allows you to list all exsting folders. 23 | #[derive(Debug, Parser)] 24 | pub struct FolderListCommand { 25 | #[command(flatten)] 26 | pub account: AccountNameFlag, 27 | 28 | /// The maximum width the table should not exceed. 29 | /// 30 | /// This argument will force the table not to exceed the given 31 | /// width, in pixels. Columns may shrink with ellipsis in order to 32 | /// fit the width. 33 | #[arg(long = "max-width", short = 'w')] 34 | #[arg(name = "table_max_width", value_name = "PIXELS")] 35 | pub table_max_width: Option, 36 | } 37 | 38 | impl FolderListCommand { 39 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 40 | info!("executing list folders command"); 41 | 42 | let (toml_account_config, account_config) = config 43 | .clone() 44 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 45 | c.account(name).ok() 46 | })?; 47 | 48 | let toml_account_config = Arc::new(toml_account_config); 49 | 50 | let backend = BackendBuilder::new( 51 | toml_account_config.clone(), 52 | Arc::new(account_config), 53 | |builder| { 54 | builder 55 | .without_features() 56 | .with_list_folders(BackendFeatureSource::Context) 57 | }, 58 | ) 59 | .without_sending_backend() 60 | .build() 61 | .await?; 62 | 63 | let folders = Folders::from(backend.list_folders().await?); 64 | let table = FoldersTable::from(folders) 65 | .with_some_width(self.table_max_width) 66 | .with_some_preset(toml_account_config.folder_list_table_preset()) 67 | .with_some_name_color(toml_account_config.folder_list_table_name_color()) 68 | .with_some_desc_color(toml_account_config.folder_list_table_desc_color()); 69 | 70 | printer.out(table)?; 71 | Ok(()) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/folder/command/mod.rs: -------------------------------------------------------------------------------- 1 | mod add; 2 | mod delete; 3 | mod expunge; 4 | mod list; 5 | mod purge; 6 | 7 | use clap::Subcommand; 8 | use color_eyre::Result; 9 | use pimalaya_tui::terminal::cli::printer::Printer; 10 | 11 | use crate::config::TomlConfig; 12 | 13 | use self::{ 14 | add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand, 15 | list::FolderListCommand, purge::FolderPurgeCommand, 16 | }; 17 | 18 | /// Create, list and purge your folders (as known as mailboxes). 19 | /// 20 | /// A folder (as known as mailbox, or directory) is a messages 21 | /// container. This subcommand allows you to manage them. 22 | #[derive(Debug, Subcommand)] 23 | pub enum FolderSubcommand { 24 | #[command(visible_alias = "create", alias = "new")] 25 | Add(FolderAddCommand), 26 | 27 | #[command(alias = "lst")] 28 | List(FolderListCommand), 29 | 30 | #[command()] 31 | Expunge(FolderExpungeCommand), 32 | 33 | #[command()] 34 | Purge(FolderPurgeCommand), 35 | 36 | #[command(alias = "remove", alias = "rm")] 37 | Delete(FolderDeleteCommand), 38 | } 39 | 40 | impl FolderSubcommand { 41 | #[allow(unused)] 42 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 43 | match self { 44 | Self::Add(cmd) => cmd.execute(printer, config).await, 45 | Self::List(cmd) => cmd.execute(printer, config).await, 46 | Self::Expunge(cmd) => cmd.execute(printer, config).await, 47 | Self::Purge(cmd) => cmd.execute(printer, config).await, 48 | Self::Delete(cmd) => cmd.execute(printer, config).await, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/folder/command/purge.rs: -------------------------------------------------------------------------------- 1 | use std::{process, sync::Arc}; 2 | 3 | use clap::Parser; 4 | use color_eyre::Result; 5 | use email::{backend::feature::BackendFeatureSource, config::Config, folder::purge::PurgeFolder}; 6 | use pimalaya_tui::{ 7 | himalaya::backend::BackendBuilder, 8 | terminal::{cli::printer::Printer, config::TomlConfig as _, prompt}, 9 | }; 10 | use tracing::info; 11 | 12 | use crate::{ 13 | account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg, 14 | }; 15 | 16 | /// Purge the given folder. 17 | /// 18 | /// All emails from the given folder are definitely deleted. The 19 | /// purged folder will remain empty after execution of the command. 20 | #[derive(Debug, Parser)] 21 | pub struct FolderPurgeCommand { 22 | #[command(flatten)] 23 | pub folder: FolderNameArg, 24 | 25 | #[command(flatten)] 26 | pub account: AccountNameFlag, 27 | 28 | #[arg(long, short)] 29 | pub yes: bool, 30 | } 31 | 32 | impl FolderPurgeCommand { 33 | pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> { 34 | info!("executing purge folder command"); 35 | 36 | let folder = &self.folder.name; 37 | 38 | if !self.yes { 39 | let confirm = format!("Do you really want to purge the folder {folder}"); 40 | let confirm = format!("{confirm}? All emails will be definitely deleted."); 41 | 42 | if !prompt::bool(confirm, false)? { 43 | process::exit(0); 44 | }; 45 | }; 46 | 47 | let (toml_account_config, account_config) = config 48 | .clone() 49 | .into_account_configs(self.account.name.as_deref(), |c: &Config, name| { 50 | c.account(name).ok() 51 | })?; 52 | 53 | let backend = BackendBuilder::new( 54 | Arc::new(toml_account_config), 55 | Arc::new(account_config), 56 | |builder| { 57 | builder 58 | .without_features() 59 | .with_purge_folder(BackendFeatureSource::Context) 60 | }, 61 | ) 62 | .without_sending_backend() 63 | .build() 64 | .await?; 65 | 66 | backend.purge_folder(folder).await?; 67 | 68 | printer.out(format!("Folder {folder} successfully purged!\n")) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/folder/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arg; 2 | pub mod command; 3 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod account; 2 | pub mod cli; 3 | pub mod completion; 4 | pub mod config; 5 | pub mod email; 6 | pub mod folder; 7 | pub mod manual; 8 | 9 | #[doc(inline)] 10 | pub use crate::email::{envelope, flag, message}; 11 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use color_eyre::Result; 3 | use himalaya::{ 4 | cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand, 5 | message::command::mailto::MessageMailtoCommand, 6 | }; 7 | use pimalaya_tui::terminal::{ 8 | cli::{printer::StdoutPrinter, tracing}, 9 | config::TomlConfig as _, 10 | }; 11 | 12 | #[tokio::main] 13 | async fn main() -> Result<()> { 14 | let tracing = tracing::install()?; 15 | 16 | #[cfg(feature = "keyring")] 17 | secret::keyring::set_global_service_name("himalaya-cli"); 18 | 19 | // if the first argument starts by "mailto:", execute straight the 20 | // mailto message command 21 | let mailto = std::env::args() 22 | .nth(1) 23 | .filter(|arg| arg.starts_with("mailto:")); 24 | 25 | if let Some(ref url) = mailto { 26 | let mut printer = StdoutPrinter::default(); 27 | let config = TomlConfig::from_default_paths().await?; 28 | 29 | return MessageMailtoCommand::new(url)? 30 | .execute(&mut printer, &config) 31 | .await; 32 | } 33 | 34 | let cli = Cli::parse(); 35 | let mut printer = StdoutPrinter::new(cli.output); 36 | let res = match cli.command { 37 | Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await, 38 | None => { 39 | let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?; 40 | EnvelopeListCommand::default() 41 | .execute(&mut printer, &config) 42 | .await 43 | } 44 | }; 45 | 46 | tracing.with_debug_and_trace_notes(res) 47 | } 48 | -------------------------------------------------------------------------------- /src/manual/command.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use clap::{CommandFactory, Parser}; 4 | use clap_mangen::Man; 5 | use color_eyre::Result; 6 | use pimalaya_tui::terminal::cli::printer::Printer; 7 | use shellexpand_utils::{canonicalize, expand}; 8 | use tracing::info; 9 | 10 | use crate::cli::Cli; 11 | 12 | /// Generate manual pages to the given directory. 13 | /// 14 | /// This command allows you to generate manual pages (following the 15 | /// man page format) to the given directory. If the directory does not 16 | /// exist, it will be created. Any existing man pages will be 17 | /// overriden. 18 | #[derive(Debug, Parser)] 19 | pub struct ManualGenerateCommand { 20 | /// Directory where man files should be generated in. 21 | #[arg(value_parser = dir_parser)] 22 | pub dir: PathBuf, 23 | } 24 | 25 | impl ManualGenerateCommand { 26 | pub async fn execute(self, printer: &mut impl Printer) -> Result<()> { 27 | info!("executing generate manuals command"); 28 | 29 | let cmd = Cli::command(); 30 | let cmd_name = cmd.get_name().to_string(); 31 | let subcmds = cmd.get_subcommands().cloned().collect::>(); 32 | let subcmds_len = subcmds.len() + 1; 33 | 34 | let mut buffer = Vec::new(); 35 | Man::new(cmd).render(&mut buffer)?; 36 | 37 | fs::create_dir_all(&self.dir)?; 38 | printer.log(format!("Generating man page for command {cmd_name}…\n"))?; 39 | fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?; 40 | 41 | for subcmd in subcmds { 42 | let subcmd_name = subcmd.get_name().to_string(); 43 | 44 | let mut buffer = Vec::new(); 45 | Man::new(subcmd).render(&mut buffer)?; 46 | 47 | printer.log(format!( 48 | "Generating man page for subcommand {subcmd_name}…\n" 49 | ))?; 50 | fs::write( 51 | self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)), 52 | buffer, 53 | )?; 54 | } 55 | 56 | printer.log(format!( 57 | "{subcmds_len} man page(s) successfully generated in {}!\n", 58 | self.dir.display() 59 | ))?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | 65 | /// Parse the given [`str`] as [`PathBuf`]. 66 | /// 67 | /// The path is first shell expanded, then canonicalized (if 68 | /// applicable). 69 | fn dir_parser(path: &str) -> Result { 70 | expand::try_path(path) 71 | .map(canonicalize::path) 72 | .map_err(|err| err.to_string()) 73 | } 74 | -------------------------------------------------------------------------------- /src/manual/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | --------------------------------------------------------------------------------