mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-24 20:10:23 +00:00
release v0.7.0 (#433)
* update codebase with email lib changes (#431) update himalaya-lib, rename remaining mbox vars add missing methods from lib update changelog * fixed missing folder aliases #430 * improve README links * fix README repology link * fix README repology table * fix README repology table 2 * center README repology table * fix README cosmetic issues * fix README cosmetic issues 2 * fix README title * fix README wiki links * fix lock file * prepare v0.6.2 * fix ci * try some musl builds #356 * add musl build to artifact #356 * add musl build to deployment pipeline #356 * migrate clap v4, add man command #419 * add option to choose color manually #407 * update links and badges * update matrix badge * add github release version badge * update badges links * fix code bloc type * fix tests * fix cargo lock * generate all man pages for all subcommands #419 * fix query and headers arg parsers * fix invalid flags and options due to clap v4 migration * fix tests * remove -l|--log-level option * refactor contributing guide * update lib * fix flags string printer * make commands read, attachments, copy, move and delete accept multiple ids * fix ids arg parser * fix flags subcommands conflicts between ids and flags * flip back copy and move arguments * add issue template (#439) * update lib, prepare for sync feature * update himalaya lib, fix senders and config * update lock file himalaya lib * fix sync enabling issues * fix wrong imap backend init in main file * fix notmuch backend post sync feature * configuration wizard (#432) * make DeserializedConfig::path more robust With this change, himalaya uses the crate `dirs` in order to follow XDG specifications on Unix, Known Folder on Windows and Standard Directories on MacOS. This gives us much smoother cross-platform support. It still has the same fallbacks (`$HOME/.config/himalaya/config.toml` and `$HOME/.himalayarc`.) Additionally, this commit removes a bit of in-house code-bloat. * add wizard entrypoint and basic structure * wip * feat: impl Serialize for all DeserializedConfigs * feat: select default account and write to file * feat: add SMTP part of wizard * build: update lockfile * refactor: separate out multiple files for wizard * style: friendlier and prettier messages * feat: add maildir part of wizard * feat: add notmuch part of wizard * chore: clippy lints and reorder prompts * fix: contrived solution to serializing None values * fix: allow empty Option field when deserializing * style: address PR review comments * fix: utilize notmuch lib in finding database path * fix notmuch wizard --------- Co-authored-by: Clément DOUIN <clement.douin@posteo.net> * add account sync progress bar * improve sync spinner * make the sync dry run flag show patches without applying them * update himalaya lib, increase imap session pool size * add disable cache flag * add nlnet logo in readme * update himalaya lib deps, make use of sync reports * prepare v0.7.0 * bump rustc v1.67.0 and clap v4.1.4 * bump himalaya lib v0.5.1, fix flake lock file --------- Co-authored-by: janabhumi <dmitriy@ideascup.me> Co-authored-by: Knut Magnus Aasrud <km@aasrud.com>
This commit is contained in:
parent
bda37ca0ed
commit
694173b534
52 changed files with 3154 additions and 1664 deletions
17
.github/ISSUE_TEMPLATE/do-not-open-issues-on-github.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/do-not-open-issues-on-github.md
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
name: Do not open issues on GitHub
|
||||
about: Instead send an email at ~soywod/pimalaya@todo.sr.ht
|
||||
title: ''
|
||||
labels: invalid
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Himalaya is slowly migrating away from GitHub. The new bug tracker is
|
||||
now on [sourcehut](https://sr.ht/). You can submit an issue either by:
|
||||
|
||||
* Sending an email at
|
||||
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht)
|
||||
(it is the simplest since you do not need to create any account)
|
||||
* Submitting [this form](https://todo.sr.ht/~soywod/pimalaya) (you
|
||||
need a free sourcehut account)
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
release_name: ${{ github.ref }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
deploy_github:
|
||||
deploy_linux_macos_windows_github:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: create_release
|
||||
strategy:
|
||||
|
@ -47,7 +47,7 @@ jobs:
|
|||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- name: Build release
|
||||
- name: Builds release
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
@ -67,6 +67,26 @@ jobs:
|
|||
asset_path: himalaya.tar.gz
|
||||
asset_name: himalaya-${{ matrix.os_name }}.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
deploy_musl_github:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Build release
|
||||
run: |
|
||||
docker run -v "${PWD}:/volume" --rm -t clux/muslrust:stable cargo build --release
|
||||
- name: Compress executable
|
||||
run: tar czf himalaya.tar.gz -C target/x86_64-unknown-linux-musl/release himalaya
|
||||
- name: Upload release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create_release.outputs.upload_url }}
|
||||
asset_path: himalaya.tar.gz
|
||||
asset_name: himalaya-musl.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
deploy_crates:
|
||||
runs-on: ubuntu-latest
|
||||
needs: create_release
|
22
.github/workflows/nix-build.yaml
vendored
22
.github/workflows/nix-build.yaml
vendored
|
@ -1,22 +0,0 @@
|
|||
name: nix-build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: cachix/install-nix-action@v13
|
||||
with:
|
||||
install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install
|
||||
install_options: '--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve'
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
- run: nix develop -c rustc --version
|
||||
- run: nix run . -- --version
|
||||
- run: nix-build
|
38
.github/workflows/nix.yml
vendored
Normal file
38
.github/workflows/nix.yml
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
name: nix
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
nix-build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkouts code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Caches Nix store
|
||||
uses: actions/cache@v3
|
||||
id: nix-cache
|
||||
with:
|
||||
path: /tmp/nix-cache
|
||||
key: nix-${{ hashFiles('**/flake.*') }}
|
||||
|
||||
- name: Installs Nix
|
||||
uses: cachix/install-nix-action@v18
|
||||
with:
|
||||
extra_nix_config: |
|
||||
experimental-features = nix-command flakes
|
||||
|
||||
- name: Imports Nix store cache
|
||||
if: ${{ steps.nix-cache.outputs.cache-hit == 'true' }}
|
||||
run: nix-store --import < /tmp/nix-cache
|
||||
|
||||
- name: Builds the project
|
||||
run: nix build
|
||||
|
||||
- name: Exports Nix store cache
|
||||
if: ${{ steps.nix-cache.outputs.cache-hit != 'true' }}
|
||||
run: nix-store --export $(find /nix/store -maxdepth 1 -name '*-*') > /tmp/nix-cache
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
-p 3465:3465 \
|
||||
-p 3993:3993 \
|
||||
-p 3995:3995 \
|
||||
greenmail/standalone:1.6.2
|
||||
greenmail/standalone:1.6.11
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
47
CHANGELOG.md
47
CHANGELOG.md
|
@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.7.0] - 2023-02-08
|
||||
|
||||
### Added
|
||||
|
||||
* Added offline support with the `account sync` command to synchronize
|
||||
a backend to a local Maildir backend [#342].
|
||||
* Added the flag `--disable-cache` to not use the local Maildir
|
||||
backend.
|
||||
* Added the email composer (from its own
|
||||
[repository](https://git.sr.ht/~soywod/mime-msg-builder)) [#341].
|
||||
* Added Musl builds to releases [#356].
|
||||
* Added `himalaya man` command to generate man page [#419].
|
||||
|
||||
### Changed
|
||||
|
||||
* Made commands `read`, `attachments`, `flags`, `copy`, `move`,
|
||||
`delete` accept multiple ids.
|
||||
* Flipped arguments `ids` and `folder` for commands `copy` and `move`
|
||||
in order the folder not to be considered as an id.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed missing folder aliases [#430].
|
||||
|
||||
### Removed
|
||||
|
||||
* Removed the `-a|--attachment` argument from `write`, `reply` and
|
||||
`forward` commands. Instead you can attach documents directly from
|
||||
the template using the syntax `<#part
|
||||
filename=/path/to/you/document.ext>`.
|
||||
* Removed the `-e|--encrypt` flag from `write`, `reply` and `forward`
|
||||
commands. Instead you can encrypt and sign parts directly from the
|
||||
template using the syntax `<#part type=text/plain encrypt=command
|
||||
sign=command>Hello!<#/part>`.
|
||||
* Removed the `-l|--log-level` option, use instead the `RUST_LOG`
|
||||
environment variable (see the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Tips:debug-and-logs))
|
||||
|
||||
## [0.6.1] - 2022-10-12
|
||||
|
||||
### Added
|
||||
|
@ -436,7 +474,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* Password from command [#22]
|
||||
* Set up README [#20]
|
||||
|
||||
[unreleased]: https://github.com/soywod/himalaya/compare/v0.6.1...HEAD
|
||||
[Unreleased]: https://github.com/soywod/himalaya/compare/v0.7.0...HEAD
|
||||
[0.7.0]: https://github.com/soywod/himalaya/compare/v0.6.2...v0.7.0
|
||||
[0.6.2]: https://github.com/soywod/himalaya/compare/v0.6.1...v0.6.2
|
||||
[0.6.1]: https://github.com/soywod/himalaya/compare/v0.6.0...v0.6.1
|
||||
[0.6.0]: https://github.com/soywod/himalaya/compare/v0.5.10...v0.6.0
|
||||
[0.5.10]: https://github.com/soywod/himalaya/compare/v0.5.9...v0.5.10
|
||||
|
@ -592,6 +632,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
[#335]: https://github.com/soywod/himalaya/issues/335
|
||||
[#338]: https://github.com/soywod/himalaya/issues/338
|
||||
[#340]: https://github.com/soywod/himalaya/issues/340
|
||||
[#341]: https://github.com/soywod/himalaya/issues/341
|
||||
[#342]: https://github.com/soywod/himalaya/issues/342
|
||||
[#344]: https://github.com/soywod/himalaya/issues/344
|
||||
[#346]: https://github.com/soywod/himalaya/issues/346
|
||||
[#352]: https://github.com/soywod/himalaya/issues/352
|
||||
[#356]: https://github.com/soywod/himalaya/issues/356
|
||||
[#419]: https://github.com/soywod/himalaya/issues/419
|
||||
[#430]: https://github.com/soywod/himalaya/issues/430
|
||||
|
|
|
@ -2,41 +2,47 @@
|
|||
|
||||
Thank you for investing your time in contributing to Himalaya!
|
||||
|
||||
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
|
||||
## Development
|
||||
|
||||
## New contributor guide
|
||||
The development environment is managed by
|
||||
[Nix](https://nixos.org/download.html). Running `nix-shell` will spawn
|
||||
a shell with everything you need to get started with the tool:
|
||||
`cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`…
|
||||
|
||||
To get an overview of the project, read the [README](README.md). To get more information about the project, read the [wiki](https://github.com/soywod/himalaya/wiki).
|
||||
```sh
|
||||
# starts a nix shell (the first launch may take a while)
|
||||
$ nix-shell
|
||||
|
||||
## Getting started
|
||||
# builds the CLI
|
||||
$ cargo build
|
||||
|
||||
### Issues
|
||||
# runs the CLI
|
||||
$ cargo run -- list
|
||||
```
|
||||
|
||||
#### Create a new issue
|
||||
## Contributing
|
||||
|
||||
If you spot a problem with the docs, [search if an issue already exists](https://github.com/soywod/himalaya/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/soywod/himalaya/issues/new/choose).
|
||||
If you find a **bug**, please send an email at
|
||||
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
|
||||
#### Solve an issue
|
||||
If you have a **question**, please send an email at
|
||||
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
|
||||
|
||||
Scan through our [existing issues](https://github.com/soywod/himalaya/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix.
|
||||
If you want to **propose a feature** or **fix a bug**, please send a
|
||||
patch at
|
||||
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht)
|
||||
using [git send-email](https://git-scm.com/docs/git-send-email) (see
|
||||
[this guide](https://git-send-email.io/) on how to configure it).
|
||||
|
||||
### Make Changes
|
||||
If you want to **subscribe** to the mailing list, please send an email
|
||||
at
|
||||
[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht).
|
||||
|
||||
#### Make changes in the UI
|
||||
If you want to **unsubscribe** to the mailing list, please send an
|
||||
email at
|
||||
[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
|
||||
|
||||
Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review.
|
||||
|
||||
#### Make changes locally
|
||||
|
||||
First, follow the instructions on [how to install Himalaya from sources](https://github.com/soywod/himalaya/wiki/Installation:sources). Then, create a working branch and start with your changes!
|
||||
|
||||
### Commit your update
|
||||
|
||||
Commit the changes once you are happy with them. Commit messages follow the [Angular Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153), but contain only a subject. The subject can be prefixed with a custom context like `msg: `, `mbox: `, `imap: ` etc.
|
||||
|
||||
> Use imperative, present tense: “change” not “changed” nor
|
||||
> “changes”<br>Don't capitalize first letter<br>No dot (.) at the end
|
||||
|
||||
### Pull Request
|
||||
|
||||
When you're finished with the changes, create a pull request, also known as a PR.
|
||||
If you want to **discuss** about the project, feel free to join the
|
||||
[Matrix](https://matrix.org/) workspace
|
||||
[#pimalaya](https://matrix.to/#/#pimalaya:matrix.org) or contact me
|
||||
directly [@soywod](https://matrix.to/#/@soywod:matrix.org).
|
||||
|
|
910
Cargo.lock
generated
910
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
43
Cargo.toml
43
Cargo.toml
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management."
|
||||
version = "0.6.1"
|
||||
version = "0.7.0"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
@ -22,25 +22,30 @@ notmuch-backend = ["himalaya-lib/notmuch-backend"]
|
|||
default = ["imap-backend", "maildir-backend"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.3.0"
|
||||
tempfile = "3.3"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.44"
|
||||
atty = "0.2.14"
|
||||
chrono = "0.4.19"
|
||||
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
||||
env_logger = "0.8.3"
|
||||
erased-serde = "0.3.18"
|
||||
himalaya-lib = "=0.4.0"
|
||||
lettre = { version = "=0.10.0-rc.7", features = ["serde"] }
|
||||
log = "0.4.14"
|
||||
mailparse = "0.13.6"
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0.61"
|
||||
shellexpand = "2.1.0"
|
||||
anyhow = "1.0"
|
||||
atty = "0.2"
|
||||
clap = "4.0"
|
||||
clap_complete = "4.0"
|
||||
clap_mangen = "0.2"
|
||||
console = "0.15.2"
|
||||
dirs = "4.0.0"
|
||||
dialoguer = "0.10.2"
|
||||
email_address = "0.2.4"
|
||||
env_logger = "0.8"
|
||||
erased-serde = "0.3"
|
||||
himalaya-lib = "0.5"
|
||||
indicatif = "0.17"
|
||||
log = "0.4"
|
||||
once_cell = "1.16.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
shellexpand = "2.1"
|
||||
termcolor = "1.1"
|
||||
terminal_size = "0.1.15"
|
||||
toml = "0.5.8"
|
||||
unicode-width = "0.1.7"
|
||||
url = "2.2.2"
|
||||
terminal_size = "0.1"
|
||||
toml = "0.5"
|
||||
unicode-width = "0.1"
|
||||
url = "2.2"
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
|
168
README.md
168
README.md
|
@ -1,25 +1,66 @@
|
|||
# 📫 Himalaya
|
||||
# 📫 Himalaya [![GitHub release](https://img.shields.io/github/v/release/soywod/himalaya?color=success&style=flat-square)](https://github.com/soywod/himalaya/releases/latest) [![Matrix](https://img.shields.io/matrix/himalaya.email.client:matrix.org?color=success&label=chat&style=flat-square)](https://matrix.to/#/#himalaya.email.client:matrix.org)
|
||||
|
||||
Command-line interface for email management based on the
|
||||
[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib).
|
||||
|
||||
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
|
||||
|
||||
*The project is under active development. Do not use in production
|
||||
before the `v1.0.0`.*
|
||||
*Warning: the project is under active development, do not use in
|
||||
production before the `v1.0.0`.*
|
||||
|
||||
## Features
|
||||
|
||||
- Folder listing
|
||||
- Email listing and searching
|
||||
- Email composition based on `$EDITOR`
|
||||
- Email manipulation (copy/move/delete)
|
||||
- Multi-accounting
|
||||
- Account listing
|
||||
- IMAP, Maildir and Notmuch support
|
||||
- IMAP IDLE mode for real-time notifications
|
||||
- PGP end-to-end encryption
|
||||
- Completions for various shells
|
||||
- JSON output
|
||||
- …
|
||||
|
||||
*Note: see the [wiki](https://github.com/soywod/himalaya/wiki) for all
|
||||
the features.*
|
||||
|
||||
## Installation
|
||||
|
||||
[![Packaging
|
||||
status](https://repology.org/badge/vertical-allrepos/himalaya.svg)](https://repology.org/project/himalaya/versions)
|
||||
<table align="center">
|
||||
<tr>
|
||||
<td width="50%">
|
||||
<a href="https://repology.org/project/himalaya/versions">
|
||||
<img src="https://repology.org/badge/vertical-allrepos/himalaya.svg" alt="Packaging status" />
|
||||
</a>
|
||||
</td>
|
||||
<td width="50%">
|
||||
|
||||
```sh
|
||||
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh
|
||||
```shell
|
||||
# Arch Linux (official)
|
||||
$ pacman -S himalaya
|
||||
|
||||
# Arch Linux (from sources)
|
||||
$ yay -S himalaya-git
|
||||
|
||||
# Homebrew
|
||||
$ brew install himalaya
|
||||
|
||||
# Cargo
|
||||
$ cargo install himalaya
|
||||
|
||||
# Nix
|
||||
$ nix-env -i himalaya
|
||||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary)
|
||||
for other installation methods.*
|
||||
*Note: see the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Installation) for other
|
||||
installation methods.*
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
## Configuration
|
||||
|
||||
|
@ -34,61 +75,96 @@ signature = "Regards,"
|
|||
default = true
|
||||
email = "test@gmail.com"
|
||||
|
||||
backend = "imap" # imap, maildir or notmuch
|
||||
backend = "imap"
|
||||
imap-host = "imap.gmail.com"
|
||||
imap-port = 993
|
||||
imap-login = "test@gmail.com"
|
||||
imap-passwd-cmd = "pass show gmail"
|
||||
imap-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||
|
||||
sender = "smtp" # smtp or sendmail
|
||||
sender = "smtp"
|
||||
smtp-host = "smtp.gmail.com"
|
||||
smtp-port = 465
|
||||
smtp-login = "test@gmail.com"
|
||||
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
|
||||
|
||||
[gmail.folder-aliases]
|
||||
inbox = "INBOX"
|
||||
sent = "[Gmail]/Sent"
|
||||
drafts = "[Gmail]/Drafts"
|
||||
|
||||
[local]
|
||||
email = "test@localhost"
|
||||
signature-delim = "~~\n"
|
||||
signature = "Regards,"
|
||||
|
||||
backend = "maildir"
|
||||
maildir-root-dir = "~/emails"
|
||||
|
||||
sender = "sendmail"
|
||||
sendmail-cmd = "msmtp --read-envelope-from --read-recipients"
|
||||
```
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file)
|
||||
for all the options.*
|
||||
*Note: see the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Configuration) for all
|
||||
the options.*
|
||||
|
||||
## Features
|
||||
## Contributing
|
||||
|
||||
- Folder listing
|
||||
- Email listing and searching
|
||||
- Email composition based on `$EDITOR`
|
||||
- Email manipulation (copy/move/delete)
|
||||
- Multi-accounting
|
||||
- Account listing
|
||||
- IMAP, Maildir and Notmuch support
|
||||
- IMAP IDLE mode for real-time notifications
|
||||
- PGP end-to-end encryption
|
||||
- Vim and Emacs plugins
|
||||
- Completions for various shells
|
||||
- JSON output
|
||||
- …
|
||||
If you find a **bug**, please send an email at
|
||||
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
|
||||
|
||||
*See the
|
||||
[wiki](https://github.com/soywod/himalaya/wiki/Usage:email:list) for
|
||||
all the features.*
|
||||
If you have a **question**, please send an email at
|
||||
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
|
||||
|
||||
If you want to **propose a feature** or **fix a bug**, please send a
|
||||
patch at
|
||||
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht)
|
||||
using [git send-email](https://git-scm.com/docs/git-send-email) (see
|
||||
[this guide](https://git-send-email.io/) on how to configure it).
|
||||
|
||||
If you want to **subscribe** to the mailing list, please send an email
|
||||
at
|
||||
[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht).
|
||||
|
||||
If you want to **unsubscribe** to the mailing list, please send an
|
||||
email at
|
||||
[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
|
||||
|
||||
If you want to **discuss** about the project, feel free to join the
|
||||
[Matrix](https://matrix.org/) workspace
|
||||
[#pimalaya](https://matrix.to/#/#pimalaya:matrix.org) or contact me
|
||||
directly [@soywod](https://matrix.to/#/@soywod:matrix.org).
|
||||
|
||||
## Credits
|
||||
|
||||
- [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib)
|
||||
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
||||
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
|
||||
- [isync](https://isync.sourceforge.io/), an email synchronizer for
|
||||
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
|
||||
|
||||
Special thanks to the
|
||||
[nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that
|
||||
helped Himalaya to receive financial support from the [NGI
|
||||
Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the
|
||||
European Commission in September, 2022.
|
||||
|
||||
* [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib)
|
||||
* [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
|
||||
* [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
|
||||
* [isync](https://isync.sourceforge.io/), an email synchronizer for
|
||||
offline usage
|
||||
- [NeoMutt](https://neomutt.org/), an email terminal user interface
|
||||
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
|
||||
* [NeoMutt](https://neomutt.org/), an email terminal user interface
|
||||
* [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
|
||||
email terminal user interface
|
||||
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool
|
||||
* [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool
|
||||
over NeoMutt and isync
|
||||
- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib
|
||||
* [rust-imap](https://github.com/jonhoo/rust-imap), a Rust IMAP
|
||||
library
|
||||
* [lettre](https://github.com/lettre/lettre), a Rust mailer library
|
||||
* [mailparse](https://github.com/staktrace/mailparse), a Rust MIME
|
||||
email parser.
|
||||
|
||||
## Sponsoring
|
||||
|
||||
[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/soywod)
|
||||
[![paypal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff&style=flat-square)](https://www.paypal.com/paypalme/soywod)
|
||||
[![ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff&style=flat-square)](https://ko-fi.com/soywod)
|
||||
[![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/soywod)
|
||||
[![liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222&style=flat-square)](https://liberapay.com/soywod)
|
||||
[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/soywod)
|
||||
[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff&style=flat-square)](https://www.paypal.com/paypalme/soywod)
|
||||
[![Ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff&style=flat-square)](https://ko-fi.com/soywod)
|
||||
[![Buy Me a Coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000&style=flat-square)](https://www.buymeacoffee.com/soywod)
|
||||
[![Liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222&style=flat-square)](https://liberapay.com/soywod)
|
||||
|
|
42
flake.lock
42
flake.lock
|
@ -3,11 +3,11 @@
|
|||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1650374568,
|
||||
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
|
||||
"lastModified": 1673956053,
|
||||
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
|
||||
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -18,11 +18,11 @@
|
|||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1656928814,
|
||||
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -36,11 +36,11 @@
|
|||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1662220400,
|
||||
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
|
||||
"lastModified": 1671096816,
|
||||
"narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
|
||||
"rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -51,11 +51,11 @@
|
|||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1664356419,
|
||||
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=",
|
||||
"lastModified": 1675698036,
|
||||
"narHash": "sha256-BgsQkQewdlQi8gapJN4phpxkI/FCE/2sORBaFcYbp/A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1",
|
||||
"rev": "1046c7b92e908a1202c0f1ba3fc21d19e1cf1b62",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -79,11 +79,11 @@
|
|||
},
|
||||
"nixpkgs_3": {
|
||||
"locked": {
|
||||
"lastModified": 1659102345,
|
||||
"narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=",
|
||||
"lastModified": 1665296151,
|
||||
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7",
|
||||
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -108,11 +108,11 @@
|
|||
"nixpkgs": "nixpkgs_3"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1664334084,
|
||||
"narHash": "sha256-cqP0TzDs3GDRprS6IgVQcWjQ0ynmjQFjYWvp+LE/s6I=",
|
||||
"lastModified": 1675823425,
|
||||
"narHash": "sha256-o/uLXQdq3OrRAv4BZVVY0VmhMmQBLWw6Y4o+p6ZiaR4=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "70eab96a255ae9b4b82b38ea5ac5c8e5b57e0abd",
|
||||
"rev": "02e1abbdcbc2d516193ff8a7add71f44cd976ba0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -123,11 +123,11 @@
|
|||
},
|
||||
"utils": {
|
||||
"locked": {
|
||||
"lastModified": 1659877975,
|
||||
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||
"lastModified": 1667395993,
|
||||
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
|
19
src/cache/args.rs
vendored
Normal file
19
src/cache/args.rs
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
//! This module provides arguments related to the cache.
|
||||
|
||||
use clap::{Arg, ArgAction, ArgMatches};
|
||||
|
||||
const ARG_DISABLE_CACHE: &str = "disable-cache";
|
||||
|
||||
/// Represents the disable cache flag argument. This argument allows
|
||||
/// the user to disable any sort of cache.
|
||||
pub fn arg() -> Arg {
|
||||
Arg::new(ARG_DISABLE_CACHE)
|
||||
.long("disable-cache")
|
||||
.help("Disable any sort of cache")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the disable cache flag parser.
|
||||
pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DISABLE_CACHE)
|
||||
}
|
1
src/cache/mod.rs
vendored
Normal file
1
src/cache/mod.rs
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod args;
|
|
@ -3,37 +3,37 @@
|
|||
//! This module provides subcommands and a command matcher related to completion.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, Shell, SubCommand};
|
||||
use log::{debug, info};
|
||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
||||
use clap_complete::Shell;
|
||||
use log::debug;
|
||||
|
||||
type OptionShell<'a> = Option<&'a str>;
|
||||
const ARG_SHELL: &str = "shell";
|
||||
const CMD_COMPLETION: &str = "completion";
|
||||
|
||||
type SomeShell = Shell;
|
||||
|
||||
/// Completion commands.
|
||||
pub enum Command<'a> {
|
||||
/// Generate completion script for the given shell slice.
|
||||
Generate(OptionShell<'a>),
|
||||
pub enum Cmd {
|
||||
/// Generate completion script for the given shell.
|
||||
Generate(SomeShell),
|
||||
}
|
||||
|
||||
/// Completion command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
|
||||
info!("entering completion command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("completion") {
|
||||
info!("completion command matched");
|
||||
let shell = m.value_of("shell");
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
|
||||
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
|
||||
debug!("shell: {:?}", shell);
|
||||
return Ok(Some(Command::Generate(shell)));
|
||||
return Ok(Some(Cmd::Generate(shell)));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Completion subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name("completion")
|
||||
.aliases(&["completions", "compl", "compe", "comp"])
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_COMPLETION)
|
||||
.about("Generates the completion script for the given shell")
|
||||
.args(&[Arg::with_name("shell")
|
||||
.possible_values(&Shell::variants()[..])
|
||||
.required(true)])]
|
||||
.args(&[Arg::new(ARG_SHELL)
|
||||
.value_parser(value_parser!(Shell))
|
||||
.required(true)])
|
||||
}
|
||||
|
|
|
@ -2,20 +2,14 @@
|
|||
//!
|
||||
//! This module gathers all completion commands.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clap::{App, Shell};
|
||||
use log::{debug, info};
|
||||
use std::{io, str::FromStr};
|
||||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use clap_complete::Shell;
|
||||
use std::io::stdout;
|
||||
|
||||
/// Generates completion script from the given [`clap::App`] for the given shell slice.
|
||||
pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> {
|
||||
info!("entering generate completion handler");
|
||||
|
||||
let shell = Shell::from_str(shell.unwrap_or_default())
|
||||
.map_err(|err| anyhow!(err))
|
||||
.context("cannot parse shell")?;
|
||||
debug!("shell: {}", shell);
|
||||
|
||||
app.gen_completions_to("himalaya", shell, &mut io::stdout());
|
||||
pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
|
||||
let name = cmd.get_name().to_string();
|
||||
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ const ARG_CONFIG: &str = "config";
|
|||
|
||||
/// Represents the config file path argument. This argument allows the
|
||||
/// user to customize the config file path.
|
||||
pub fn arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_CONFIG)
|
||||
pub fn arg() -> Arg {
|
||||
Arg::new(ARG_CONFIG)
|
||||
.long("config")
|
||||
.short("c")
|
||||
.short('c')
|
||||
.help("Forces a specific config file path")
|
||||
.value_name("PATH")
|
||||
}
|
||||
|
||||
/// Represents the config file path argument parser.
|
||||
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> {
|
||||
matches.value_of(ARG_CONFIG)
|
||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
|
||||
}
|
||||
|
|
|
@ -4,16 +4,20 @@
|
|||
//! user configuration file.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use dirs::{config_dir, home_dir};
|
||||
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
|
||||
use log::{debug, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, fs, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::{account::DeserializedAccountConfig, config::prelude::*};
|
||||
use crate::{
|
||||
account::DeserializedAccountConfig,
|
||||
config::{prelude::*, wizard::wizard},
|
||||
};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedConfig {
|
||||
#[serde(alias = "name")]
|
||||
|
@ -27,11 +31,14 @@ pub struct DeserializedConfig {
|
|||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
#[serde(default, with = "EmailTextPlainFormatOptionDef", skip_serializing_if = "Option::is_none")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_verify_cmd: Option<String>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
pub email_writing_sign_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
#[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
|
||||
#[serde(flatten)]
|
||||
|
@ -41,74 +48,52 @@ pub struct DeserializedConfig {
|
|||
impl DeserializedConfig {
|
||||
/// Tries to create a config from an optional path.
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
|
||||
trace!(">> parse config from path");
|
||||
debug!("path: {:?}", path);
|
||||
|
||||
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
let config: Self = toml::from_str(&content).context("cannot parse config file")?;
|
||||
let config: Self = match path.map(|s| s.into()).or_else(Self::path) {
|
||||
Some(path) => {
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
toml::from_str(&content).context("cannot parse config file")?
|
||||
}
|
||||
None => wizard()?,
|
||||
};
|
||||
|
||||
if config.accounts.is_empty() {
|
||||
return Err(anyhow!("config file must contain at least one account"));
|
||||
}
|
||||
|
||||
trace!("config: {:?}", config);
|
||||
trace!("<< parse config from path");
|
||||
trace!("config: {:#?}", config);
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from XDG_CONFIG_HOME
|
||||
/// environment variable.
|
||||
fn path_from_xdg() -> Result<PathBuf> {
|
||||
let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?;
|
||||
let path = PathBuf::from(path).join("himalaya").join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from HOME environment
|
||||
/// variable.
|
||||
fn path_from_xdg_alt() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path)
|
||||
.join(".config")
|
||||
.join("himalaya")
|
||||
.join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the .himalayarc config file path from HOME
|
||||
/// environment variable.
|
||||
fn path_from_home() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path).join(".himalayarc");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the config file path.
|
||||
pub fn path() -> Result<PathBuf> {
|
||||
Self::path_from_xdg()
|
||||
.or_else(|_| Self::path_from_xdg_alt())
|
||||
.or_else(|_| Self::path_from_home())
|
||||
/// Tries to return a config path from a few default settings.
|
||||
///
|
||||
/// Tries paths in this order:
|
||||
///
|
||||
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
|
||||
/// OSes.)
|
||||
/// - `"$HOME/.config/himalaya/config.toml"`
|
||||
/// - `"$HOME/.himalayarc"`
|
||||
///
|
||||
/// Returns `Some(path)` if the path exists, otherwise `None`.
|
||||
pub fn path() -> Option<PathBuf> {
|
||||
config_dir()
|
||||
.map(|p| p.join("himalaya").join("config.toml"))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
|
||||
.filter(|p| p.exists())
|
||||
}
|
||||
|
||||
pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> {
|
||||
let (account_config, backend_config) = match account_name {
|
||||
let (account_name, deserialized_account_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(_, account)| {
|
||||
.find_map(|(name, account)| {
|
||||
if account.is_default() {
|
||||
Some(account)
|
||||
Some((name.clone(), account))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -117,9 +102,12 @@ impl DeserializedConfig {
|
|||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.map(|account| (name.to_string(), account))
|
||||
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
|
||||
}?
|
||||
.to_configs(self);
|
||||
}?;
|
||||
|
||||
let (account_config, backend_config) =
|
||||
deserialized_account_config.to_configs(account_name, self);
|
||||
|
||||
Ok((account_config, backend_config))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod prelude;
|
||||
mod wizard;
|
||||
|
||||
pub use config::*;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig};
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
|
@ -11,7 +11,7 @@ use himalaya_lib::MaildirConfig;
|
|||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "SmtpConfig")]
|
||||
struct SmtpConfigDef {
|
||||
#[serde(rename = "smtp-host")]
|
||||
|
@ -31,7 +31,7 @@ struct SmtpConfigDef {
|
|||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "ImapConfig")]
|
||||
pub struct ImapConfigDef {
|
||||
#[serde(rename = "imap-host")]
|
||||
|
@ -57,7 +57,7 @@ pub struct ImapConfigDef {
|
|||
}
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "MaildirConfig")]
|
||||
pub struct MaildirConfigDef {
|
||||
#[serde(rename = "maildir-root-dir")]
|
||||
|
@ -65,40 +65,31 @@ pub struct MaildirConfigDef {
|
|||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "NotmuchConfig")]
|
||||
pub struct NotmuchConfigDef {
|
||||
#[serde(rename = "notmuch-db-path")]
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "Option<EmailTextPlainFormat>")]
|
||||
pub enum EmailTextPlainFormatOptionDef {
|
||||
#[serde(with = "EmailTextPlainFormatDef")]
|
||||
Some(EmailTextPlainFormat),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
|
||||
enum EmailTextPlainFormatDef {
|
||||
pub enum EmailTextPlainFormatDef {
|
||||
Auto,
|
||||
Flowed,
|
||||
Fixed(usize),
|
||||
}
|
||||
|
||||
pub mod email_text_plain_format {
|
||||
use himalaya_lib::EmailTextPlainFormat;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailTextPlainFormatDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailTextPlainFormat>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
|
||||
pub enum EmailSenderDef {
|
||||
None,
|
||||
|
@ -108,36 +99,27 @@ pub enum EmailSenderDef {
|
|||
Sendmail(SendmailConfig),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "SendmailConfig")]
|
||||
pub struct SendmailConfigDef {
|
||||
#[serde(rename = "sendmail-cmd")]
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "Option<EmailHooks>")]
|
||||
pub enum EmailHooksOptionDef {
|
||||
#[serde(with = "EmailHooksDef")]
|
||||
Some(EmailHooks),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
/// Represents the email hooks. Useful for doing extra email
|
||||
/// processing before or after sending it.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(remote = "EmailHooks")]
|
||||
struct EmailHooksDef {
|
||||
pub struct EmailHooksDef {
|
||||
/// Represents the hook called just before sending an email.
|
||||
pub pre_send: Option<String>,
|
||||
}
|
||||
|
||||
pub mod email_hooks {
|
||||
use himalaya_lib::EmailHooks;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailHooksDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailHooks>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
||||
|
|
57
src/config/wizard/imap.rs
Normal file
57
src/config/wizard/imap.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use super::{SECURITY_PROTOCOLS, THEME};
|
||||
use crate::account::{
|
||||
DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedImapAccountConfig,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use dialoguer::{Input, Select};
|
||||
use himalaya_lib::ImapConfig;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
|
||||
// TODO: Validate by checking as valid URI
|
||||
let mut backend = ImapConfig {
|
||||
host: Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter the IMAP host:")
|
||||
.default(format!("imap.{}", base.email.rsplit_once('@').unwrap().1))
|
||||
.interact()?,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let default_port = match Select::with_theme(&*THEME)
|
||||
.with_prompt("Which security protocol do you want to use?")
|
||||
.items(SECURITY_PROTOCOLS)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(idx) if SECURITY_PROTOCOLS[idx] == "SSL/TLS" => {
|
||||
backend.ssl = Some(true);
|
||||
993
|
||||
}
|
||||
Some(idx) if SECURITY_PROTOCOLS[idx] == "STARTTLS" => {
|
||||
backend.starttls = Some(true);
|
||||
143
|
||||
}
|
||||
_ => 143,
|
||||
};
|
||||
|
||||
backend.port = Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter the IMAP port:")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
backend.login = Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter your IMAP login:")
|
||||
.default(base.email.clone())
|
||||
.interact()?;
|
||||
|
||||
backend.passwd_cmd = Input::with_theme(&*THEME)
|
||||
.with_prompt("What shell command should we run to get your password?")
|
||||
.default(format!("pass show {}", &base.email))
|
||||
.interact()?;
|
||||
|
||||
Ok(DeserializedAccountConfig::Imap(
|
||||
DeserializedImapAccountConfig { base, backend },
|
||||
))
|
||||
}
|
31
src/config/wizard/maildir.rs
Normal file
31
src/config/wizard/maildir.rs
Normal file
|
@ -0,0 +1,31 @@
|
|||
use super::THEME;
|
||||
use crate::account::{
|
||||
DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedMaildirAccountConfig,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use dirs::home_dir;
|
||||
use himalaya_lib::MaildirConfig;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
|
||||
let input = if let Some(home) = home_dir() {
|
||||
Input::with_theme(&*THEME)
|
||||
.default(home.join("Mail").display().to_string())
|
||||
.with_prompt("Enter the path to your maildir")
|
||||
.interact_text()?
|
||||
} else {
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter the path to your maildir")
|
||||
.interact_text()?
|
||||
};
|
||||
|
||||
Ok(DeserializedAccountConfig::Maildir(
|
||||
DeserializedMaildirAccountConfig {
|
||||
base,
|
||||
backend: MaildirConfig {
|
||||
root_dir: input.into(),
|
||||
},
|
||||
},
|
||||
))
|
||||
}
|
170
src/config/wizard/mod.rs
Normal file
170
src/config/wizard/mod.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
#[cfg(feature = "imap-backend")]
|
||||
mod imap;
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
mod maildir;
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
mod notmuch;
|
||||
mod sendmail;
|
||||
mod smtp;
|
||||
mod validators;
|
||||
|
||||
use super::DeserializedConfig;
|
||||
use crate::account::{DeserializedAccountConfig, DeserializedBaseAccountConfig};
|
||||
use anyhow::{anyhow, Result};
|
||||
use console::style;
|
||||
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Select};
|
||||
use log::trace;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{fs, process};
|
||||
|
||||
const BACKENDS: &[&str] = &[
|
||||
#[cfg(feature = "imap-backend")]
|
||||
"IMAP",
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
"Maildir",
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
"Notmuch",
|
||||
];
|
||||
|
||||
const SENDERS: &[&str] = &["SMTP", "Sendmail"];
|
||||
|
||||
const SECURITY_PROTOCOLS: &[&str] = &["SSL/TLS", "STARTTLS", "None"];
|
||||
|
||||
// A wizard should have pretty colors 💅
|
||||
static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
|
||||
|
||||
pub(crate) fn wizard() -> Result<DeserializedConfig> {
|
||||
trace!(">> wizard");
|
||||
println!("Himalaya couldn't find an already existing configuration file.");
|
||||
|
||||
match Confirm::new()
|
||||
.with_prompt("Do you want to create one with the wizard?")
|
||||
.default(true)
|
||||
.report(false)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(false) | None => process::exit(0),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Determine path to save to
|
||||
let path = dirs::config_dir()
|
||||
.map(|p| p.join("himalaya").join("config.toml"))
|
||||
.ok_or_else(|| anyhow!("The wizard could not determine the config directory. Aborting"))?;
|
||||
|
||||
let mut config = DeserializedConfig::default();
|
||||
|
||||
// Setup one or multiple accounts
|
||||
println!("\n{}", style("First let's setup an account").underlined());
|
||||
while let Some(account_config) = configure_account()? {
|
||||
let name: String = Input::with_theme(&*THEME)
|
||||
.with_prompt("What would you like to name your account?")
|
||||
.default("Personal".to_owned())
|
||||
.interact()?;
|
||||
|
||||
config.accounts.insert(name, account_config);
|
||||
|
||||
match Confirm::new()
|
||||
.with_prompt("Setup another account?")
|
||||
.default(false)
|
||||
.report(false)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(true) => println!("\n{}", style("Setting up another account").underlined()),
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
||||
// If one acounts is setup, make it the default. If multiple accounts are setup, decide which
|
||||
// will be the default. If no accounts are setup, exit the process
|
||||
let default = match config.accounts.len() {
|
||||
1 => Some(config.accounts.values_mut().next().unwrap()),
|
||||
i if i > 1 => {
|
||||
let accounts = config.accounts.clone();
|
||||
let accounts: Vec<&String> = accounts.keys().collect();
|
||||
|
||||
println!(
|
||||
"\n{}",
|
||||
style(format!("You've setup {} accounts", accounts.len())).underlined()
|
||||
);
|
||||
match Select::with_theme(&*THEME)
|
||||
.with_prompt("Which account would you like to set as your default?")
|
||||
.items(&accounts)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(i) => Some(config.accounts.get_mut(accounts[i]).unwrap()),
|
||||
_ => process::exit(0),
|
||||
}
|
||||
}
|
||||
_ => process::exit(0),
|
||||
};
|
||||
|
||||
match default {
|
||||
Some(DeserializedAccountConfig::None(default)) => default.default = Some(true),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Some(DeserializedAccountConfig::Imap(default)) => default.base.default = Some(true),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Some(DeserializedAccountConfig::Maildir(default)) => default.base.default = Some(true),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Some(DeserializedAccountConfig::Notmuch(default)) => default.base.default = Some(true),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Serialize config to file
|
||||
println!("\nWriting the configuration to {path:?}...");
|
||||
fs::create_dir_all(path.parent().unwrap())?;
|
||||
fs::write(path, toml::to_vec(&config)?)?;
|
||||
|
||||
trace!("<< wizard");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn configure_account() -> Result<Option<DeserializedAccountConfig>> {
|
||||
let mut base = configure_base()?;
|
||||
let sender = Select::with_theme(&*THEME)
|
||||
.with_prompt("Which sender would you like use with your account?")
|
||||
.items(SENDERS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
base.email_sender = match sender {
|
||||
Some(idx) if SENDERS[idx] == "SMTP" => smtp::configure(&base),
|
||||
Some(idx) if SENDERS[idx] == "Sendmail" => sendmail::configure(),
|
||||
_ => return Ok(None),
|
||||
}?;
|
||||
|
||||
let backend = Select::with_theme(&*THEME)
|
||||
.with_prompt("Which backend would you like to configure your account for?")
|
||||
.items(BACKENDS)
|
||||
.default(0)
|
||||
.interact_opt()?;
|
||||
|
||||
match backend {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Some(idx) if BACKENDS[idx] == "IMAP" => Ok(Some(imap::configure(base)?)),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Some(idx) if BACKENDS[idx] == "Maildir" => Ok(Some(maildir::configure(base)?)),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Some(idx) if BACKENDS[idx] == "Notmuch" => Ok(Some(notmuch::configure(base)?)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_base() -> Result<DeserializedBaseAccountConfig> {
|
||||
let mut base_account_config = DeserializedBaseAccountConfig {
|
||||
email: Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter your email:")
|
||||
.validate_with(validators::EmailValidator)
|
||||
.interact()?,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
base_account_config.display_name = Some(
|
||||
Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter display name:")
|
||||
.interact()?,
|
||||
);
|
||||
|
||||
Ok(base_account_config)
|
||||
}
|
25
src/config/wizard/notmuch.rs
Normal file
25
src/config/wizard/notmuch.rs
Normal file
|
@ -0,0 +1,25 @@
|
|||
use super::THEME;
|
||||
use crate::account::{
|
||||
DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedNotmuchAccountConfig,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use himalaya_lib::{NotmuchBackend, NotmuchConfig};
|
||||
|
||||
pub(crate) fn configure(base: DeserializedBaseAccountConfig) -> Result<DeserializedAccountConfig> {
|
||||
let db_path = match NotmuchBackend::get_default_db_path() {
|
||||
Ok(db) => db,
|
||||
_ => {
|
||||
let input: String = Input::with_theme(&*THEME)
|
||||
.with_prompt("Could not find a notmuch database. Enter path manually:")
|
||||
.interact_text()?;
|
||||
input.into()
|
||||
}
|
||||
};
|
||||
|
||||
let backend = NotmuchConfig { db_path };
|
||||
|
||||
Ok(DeserializedAccountConfig::Notmuch(
|
||||
DeserializedNotmuchAccountConfig { base, backend },
|
||||
))
|
||||
}
|
13
src/config/wizard/sendmail.rs
Normal file
13
src/config/wizard/sendmail.rs
Normal file
|
@ -0,0 +1,13 @@
|
|||
use super::THEME;
|
||||
use anyhow::Result;
|
||||
use dialoguer::Input;
|
||||
use himalaya_lib::{EmailSender, SendmailConfig};
|
||||
|
||||
pub(crate) fn configure() -> Result<EmailSender> {
|
||||
Ok(EmailSender::Sendmail(SendmailConfig {
|
||||
cmd: Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter an external command to send a mail: ")
|
||||
.default("/usr/bin/msmtp".to_owned())
|
||||
.interact()?,
|
||||
}))
|
||||
}
|
51
src/config/wizard/smtp.rs
Normal file
51
src/config/wizard/smtp.rs
Normal file
|
@ -0,0 +1,51 @@
|
|||
use super::{SECURITY_PROTOCOLS, THEME};
|
||||
use crate::account::DeserializedBaseAccountConfig;
|
||||
use anyhow::Result;
|
||||
use dialoguer::{Input, Select};
|
||||
use himalaya_lib::{EmailSender, SmtpConfig};
|
||||
|
||||
pub(crate) fn configure(base: &DeserializedBaseAccountConfig) -> Result<EmailSender> {
|
||||
let mut smtp_config = SmtpConfig {
|
||||
host: Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter the SMTP host: ")
|
||||
.default(format!("smtp.{}", base.email.rsplit_once('@').unwrap().1))
|
||||
.interact()?,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let default_port = match Select::with_theme(&*THEME)
|
||||
.with_prompt("Which security protocol do you want to use?")
|
||||
.items(SECURITY_PROTOCOLS)
|
||||
.default(0)
|
||||
.interact_opt()?
|
||||
{
|
||||
Some(idx) if SECURITY_PROTOCOLS[idx] == "SSL/TLS" => {
|
||||
smtp_config.ssl = Some(true);
|
||||
465
|
||||
}
|
||||
Some(idx) if SECURITY_PROTOCOLS[idx] == "STARTTLS" => {
|
||||
smtp_config.starttls = Some(true);
|
||||
587
|
||||
}
|
||||
_ => 25,
|
||||
};
|
||||
|
||||
smtp_config.port = Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter the SMTP port:")
|
||||
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
|
||||
.default(default_port.to_string())
|
||||
.interact()
|
||||
.map(|input| input.parse::<u16>().unwrap())?;
|
||||
|
||||
smtp_config.login = Input::with_theme(&*THEME)
|
||||
.with_prompt("Enter your SMTP login:")
|
||||
.default(base.email.clone())
|
||||
.interact()?;
|
||||
|
||||
smtp_config.passwd_cmd = Input::with_theme(&*THEME)
|
||||
.with_prompt("What shell command should we run to get your password?")
|
||||
.default(format!("pass show {}", &base.email))
|
||||
.interact()?;
|
||||
|
||||
Ok(EmailSender::Smtp(smtp_config))
|
||||
}
|
18
src/config/wizard/validators.rs
Normal file
18
src/config/wizard/validators.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
use anyhow::anyhow;
|
||||
use dialoguer::Validator;
|
||||
use email_address::EmailAddress;
|
||||
|
||||
pub(crate) struct EmailValidator;
|
||||
|
||||
impl<T: ToString> Validator<T> for EmailValidator {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn validate(&mut self, input: &T) -> Result<(), Self::Err> {
|
||||
let input = input.to_string();
|
||||
if EmailAddress::is_valid(&input) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("Invalid email address: {}", input))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +1,43 @@
|
|||
//! This module provides arguments related to the user account config.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
use log::info;
|
||||
|
||||
use crate::ui::table;
|
||||
|
||||
const ARG_ACCOUNT: &str = "account";
|
||||
const ARG_DRY_RUN: &str = "dry-run";
|
||||
const CMD_ACCOUNTS: &str = "accounts";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_SYNC: &str = "sync";
|
||||
|
||||
type DryRun = bool;
|
||||
|
||||
/// Represents the account commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd {
|
||||
/// Represents the list accounts command.
|
||||
List(table::args::MaxTableWidth),
|
||||
/// Represents the sync account command.
|
||||
Sync(DryRun),
|
||||
}
|
||||
|
||||
/// Represents the account command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
|
||||
info!("accounts command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
if let Some(m) = m.subcommand_matches(CMD_SYNC) {
|
||||
info!("sync account subcommand matched");
|
||||
let dry_run = parse_dry_run_arg(m);
|
||||
Some(Cmd::Sync(dry_run))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
info!("list accounts subcommand matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
Some(Cmd::List(max_table_width))
|
||||
} else {
|
||||
info!("no account subcommand matched, falling back to subcommand list");
|
||||
Some(Cmd::List(None))
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -29,25 +45,50 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents the account subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_ACCOUNTS)
|
||||
.aliases(&["account", "acc", "a"])
|
||||
.about("Lists accounts")
|
||||
.arg(table::args::max_width())]
|
||||
/// Represents the account subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_ACCOUNTS)
|
||||
.about("Manage accounts")
|
||||
.subcommands([
|
||||
Command::new(CMD_LIST)
|
||||
.about("List all accounts from the config file")
|
||||
.arg(table::args::max_width()),
|
||||
Command::new(CMD_SYNC)
|
||||
.about("Synchronize the given account locally")
|
||||
.arg(dry_run()),
|
||||
])
|
||||
}
|
||||
|
||||
/// Represents the user account name argument. This argument allows
|
||||
/// the user to select a different account than the default one.
|
||||
pub fn arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ACCOUNT)
|
||||
pub fn arg() -> Arg {
|
||||
Arg::new(ARG_ACCOUNT)
|
||||
.long("account")
|
||||
.short("a")
|
||||
.help("Selects a specific account")
|
||||
.short('a')
|
||||
.help("Select a specific account by name")
|
||||
.value_name("STRING")
|
||||
}
|
||||
|
||||
/// Represents the user account name argument parser.
|
||||
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> {
|
||||
matches.value_of(ARG_ACCOUNT)
|
||||
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
|
||||
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag. This flag allows
|
||||
/// the user to see the changes of a sync without applying them.
|
||||
pub fn dry_run() -> Arg {
|
||||
Arg::new(ARG_DRY_RUN)
|
||||
.help("Do not apply changes of the synchronization")
|
||||
.long_help(
|
||||
"Do not apply changes of the synchronization.
|
||||
Changes can be visualized with the RUST_LOG=trace environment variable.",
|
||||
)
|
||||
.short('d')
|
||||
.long("dry-run")
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the user account sync dry run flag parser.
|
||||
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_DRY_RUN)
|
||||
}
|
||||
|
|
|
@ -14,13 +14,13 @@ use himalaya_lib::MaildirConfig;
|
|||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{prelude::*, DeserializedConfig};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "backend", rename_all = "snake_case")]
|
||||
pub enum DeserializedAccountConfig {
|
||||
None(DeserializedBaseAccountConfig),
|
||||
|
@ -33,25 +33,30 @@ pub enum DeserializedAccountConfig {
|
|||
}
|
||||
|
||||
impl DeserializedAccountConfig {
|
||||
pub fn to_configs(&self, global_config: &DeserializedConfig) -> (AccountConfig, BackendConfig) {
|
||||
pub fn to_configs(
|
||||
&self,
|
||||
name: String,
|
||||
global_config: &DeserializedConfig,
|
||||
) -> (AccountConfig, BackendConfig) {
|
||||
match self {
|
||||
DeserializedAccountConfig::None(config) => {
|
||||
(config.to_account_config(global_config), BackendConfig::None)
|
||||
}
|
||||
DeserializedAccountConfig::None(config) => (
|
||||
config.to_account_config(name, global_config),
|
||||
BackendConfig::None,
|
||||
),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Imap(&config.backend),
|
||||
config.base.to_account_config(name, global_config),
|
||||
BackendConfig::Imap(config.backend.clone()),
|
||||
),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Maildir(&config.backend),
|
||||
config.base.to_account_config(name, global_config),
|
||||
BackendConfig::Maildir(config.backend.clone()),
|
||||
),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Notmuch(&config.backend),
|
||||
config.base.to_account_config(name, global_config),
|
||||
BackendConfig::Notmuch(config.backend.clone()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +74,7 @@ impl DeserializedAccountConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedBaseAccountConfig {
|
||||
pub email: String,
|
||||
|
@ -84,18 +89,25 @@ pub struct DeserializedBaseAccountConfig {
|
|||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
#[serde(default, with = "EmailTextPlainFormatOptionDef", skip_serializing_if = "Option::is_none")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_verify_cmd: Option<String>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_headers: Option<Vec<String>>,
|
||||
pub email_writing_sign_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(flatten, with = "EmailSenderDef")]
|
||||
pub email_sender: EmailSender,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
#[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
|
||||
#[serde(default)]
|
||||
pub sync: bool,
|
||||
pub sync_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl DeserializedBaseAccountConfig {
|
||||
pub fn to_account_config(&self, config: &DeserializedConfig) -> AccountConfig {
|
||||
pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig {
|
||||
let mut folder_aliases = config
|
||||
.folder_aliases
|
||||
.as_ref()
|
||||
|
@ -109,6 +121,7 @@ impl DeserializedBaseAccountConfig {
|
|||
);
|
||||
|
||||
AccountConfig {
|
||||
name,
|
||||
email: self.email.to_owned(),
|
||||
display_name: self
|
||||
.display_name
|
||||
|
@ -148,6 +161,16 @@ impl DeserializedBaseAccountConfig {
|
|||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
email_reading_verify_cmd: self
|
||||
.email_reading_verify_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_reading_verify_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_reading_decrypt_cmd: self
|
||||
.email_reading_decrypt_cmd
|
||||
.as_ref()
|
||||
|
@ -158,6 +181,16 @@ impl DeserializedBaseAccountConfig {
|
|||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_sign_cmd: self
|
||||
.email_writing_sign_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_writing_sign_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_encrypt_cmd: self
|
||||
.email_writing_encrypt_cmd
|
||||
.as_ref()
|
||||
|
@ -168,6 +201,11 @@ impl DeserializedBaseAccountConfig {
|
|||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_headers: self
|
||||
.email_writing_headers
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)),
|
||||
email_sender: self.email_sender.to_owned(),
|
||||
email_hooks: EmailHooks {
|
||||
pre_send: self
|
||||
|
@ -183,11 +221,13 @@ impl DeserializedBaseAccountConfig {
|
|||
})
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
sync: self.sync,
|
||||
sync_dir: self.sync_dir.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub struct DeserializedImapAccountConfig {
|
||||
#[serde(flatten)]
|
||||
|
@ -196,7 +236,7 @@ pub struct DeserializedImapAccountConfig {
|
|||
pub backend: ImapConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub struct DeserializedMaildirAccountConfig {
|
||||
#[serde(flatten)]
|
||||
|
@ -205,7 +245,7 @@ pub struct DeserializedMaildirAccountConfig {
|
|||
pub backend: MaildirConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub struct DeserializedNotmuchAccountConfig {
|
||||
#[serde(flatten)]
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::AccountConfig;
|
||||
use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent};
|
||||
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
|
@ -19,7 +20,7 @@ pub fn list<'a, P: Printer>(
|
|||
deserialized_config: &DeserializedConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!(">> account list handler");
|
||||
info!("entering the list accounts handler");
|
||||
|
||||
let accounts: Accounts = deserialized_config.accounts.iter().into();
|
||||
trace!("accounts: {:?}", accounts);
|
||||
|
@ -36,6 +37,184 @@ pub fn list<'a, P: Printer>(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Synchronizes the account defined using argument `-a|--account`. If
|
||||
/// no account given, synchronizes the default one.
|
||||
pub fn sync<P: Printer>(
|
||||
account_config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &dyn Backend,
|
||||
dry_run: bool,
|
||||
) -> Result<()> {
|
||||
info!("entering the sync accounts handler");
|
||||
trace!("dry run: {}", dry_run);
|
||||
|
||||
let sync_builder = BackendSyncBuilder::new(account_config);
|
||||
|
||||
if dry_run {
|
||||
let report = sync_builder.dry_run(true).sync(backend)?;
|
||||
let mut hunks_count = report.folders_patch.len();
|
||||
|
||||
if !report.folders_patch.is_empty() {
|
||||
printer.print_log("Folders patch:")?;
|
||||
for (hunk, _) in report.folders_patch {
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
if !report.envelopes_patch.is_empty() {
|
||||
printer.print_log("Envelopes patch:")?;
|
||||
for (hunk, _) in report.envelopes_patch {
|
||||
hunks_count += 1;
|
||||
printer.print_log(format!(" - {hunk}"))?;
|
||||
}
|
||||
printer.print_log("")?;
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Estimated patch length for account {} to be synchronized: {hunks_count}",
|
||||
backend.name(),
|
||||
))?;
|
||||
} else if printer.is_json() {
|
||||
sync_builder.sync(backend)?;
|
||||
printer.print(format!(
|
||||
"Account {} successfully synchronized!",
|
||||
backend.name()
|
||||
))?;
|
||||
} else {
|
||||
let multi = MultiProgress::new();
|
||||
let progress = multi.add(
|
||||
ProgressBar::new(0).with_style(
|
||||
ProgressStyle::with_template(
|
||||
" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} {pos}/{len} ",
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
);
|
||||
|
||||
let report = sync_builder
|
||||
.on_progress(|evt| {
|
||||
use BackendSyncProgressEvent::*;
|
||||
Ok(match evt {
|
||||
GetLocalCachedFolders => {
|
||||
progress.set_length(4);
|
||||
progress.set_position(0);
|
||||
progress.set_message("Getting local cached folders…");
|
||||
}
|
||||
GetLocalFolders => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting local maildir folders…");
|
||||
}
|
||||
GetRemoteCachedFolders => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting remote cached folders…");
|
||||
}
|
||||
GetRemoteFolders => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting remote folders…");
|
||||
}
|
||||
BuildFoldersPatch => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Building patch…");
|
||||
}
|
||||
ProcessFoldersPatch(n) => {
|
||||
progress.set_length(n as u64);
|
||||
progress.set_position(0);
|
||||
progress.set_message("Processing patch…");
|
||||
}
|
||||
ProcessFolderHunk(msg) => {
|
||||
progress.inc(1);
|
||||
progress.set_message(msg + "…");
|
||||
}
|
||||
StartEnvelopesSync(folder, n, len) => {
|
||||
multi.println(format!("[{n:2}/{len}] {folder}")).unwrap();
|
||||
progress.reset();
|
||||
}
|
||||
GetLocalCachedEnvelopes => {
|
||||
progress.set_length(4);
|
||||
progress.set_message("Getting local cached envelopes…");
|
||||
}
|
||||
GetLocalEnvelopes => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting local maildir envelopes…");
|
||||
}
|
||||
GetRemoteCachedEnvelopes => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting remote cached envelopes…");
|
||||
}
|
||||
GetRemoteEnvelopes => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Getting remote envelopes…");
|
||||
}
|
||||
BuildEnvelopesPatch => {
|
||||
progress.inc(1);
|
||||
progress.set_message("Building patch…");
|
||||
}
|
||||
ProcessEnvelopesPatch(n) => {
|
||||
progress.set_length(n as u64);
|
||||
progress.set_position(0);
|
||||
progress.set_message("Processing patch…");
|
||||
}
|
||||
ProcessEnvelopeHunk(msg) => {
|
||||
progress.inc(1);
|
||||
progress.set_message(msg + "…");
|
||||
}
|
||||
})
|
||||
})
|
||||
.sync(backend)?;
|
||||
|
||||
progress.finish_and_clear();
|
||||
|
||||
let folders_patch_err = report
|
||||
.folders_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !folders_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occured while applying the folders patch:")?;
|
||||
folders_patch_err
|
||||
.iter()
|
||||
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
|
||||
}
|
||||
|
||||
if let Some(err) = report.folders_cache_patch.1 {
|
||||
printer.print_log("")?;
|
||||
printer.print_log(format!(
|
||||
"Error occured while applying the folder cache patch: {err}"
|
||||
))?;
|
||||
}
|
||||
|
||||
let envelopes_patch_err = report
|
||||
.envelopes_patch
|
||||
.iter()
|
||||
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
|
||||
.collect::<Vec<_>>();
|
||||
if !envelopes_patch_err.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Errors occured while applying the envelopes patch:")?;
|
||||
for (hunk, err) in folders_patch_err {
|
||||
printer.print_log(format!(" - {hunk}: {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
if !report.envelopes_cache_patch.1.is_empty() {
|
||||
printer.print_log("")?;
|
||||
printer.print_log("Error occured while applying the envelopes cache patch:")?;
|
||||
for err in report.envelopes_cache_patch.1 {
|
||||
printer.print_log(format!(" - {err}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print(format!(
|
||||
"Account {} successfully synchronized!",
|
||||
backend.name()
|
||||
))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{AccountConfig, ImapConfig};
|
||||
|
@ -101,13 +280,10 @@ mod tests {
|
|||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> Result<()> {
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn is_json(&self) -> bool {
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
//! Module related to email CLI.
|
||||
//! Email CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to email.
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::email::TplOverride;
|
||||
use log::{debug, trace};
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
|
||||
use crate::{email, flag, folder, tpl, ui::table};
|
||||
use crate::{flag, folder, tpl, ui::table};
|
||||
|
||||
const ARG_ATTACHMENTS: &str = "attachment";
|
||||
const ARG_CRITERIA: &str = "criterion";
|
||||
const ARG_ENCRYPT: &str = "encrypt";
|
||||
const ARG_HEADERS: &str = "header";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
const ARG_ID: &str = "id";
|
||||
const ARG_IDS: &str = "ids";
|
||||
const ARG_MIME_TYPE: &str = "mime-type";
|
||||
|
@ -24,7 +21,7 @@ const ARG_REPLY_ALL: &str = "reply-all";
|
|||
const ARG_SANITIZE: &str = "sanitize";
|
||||
const CMD_ATTACHMENTS: &str = "attachments";
|
||||
const CMD_COPY: &str = "copy";
|
||||
const CMD_DEL: &str = "delete";
|
||||
const CMD_DELETE: &str = "delete";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_LIST: &str = "list";
|
||||
const CMD_MOVE: &str = "move";
|
||||
|
@ -36,37 +33,35 @@ const CMD_SEND: &str = "send";
|
|||
const CMD_SORT: &str = "sort";
|
||||
const CMD_WRITE: &str = "write";
|
||||
|
||||
type Criteria = String;
|
||||
type Encrypt = bool;
|
||||
type Folder<'a> = &'a str;
|
||||
type Page = usize;
|
||||
type PageSize = usize;
|
||||
type Query = String;
|
||||
type Sanitize = bool;
|
||||
type Raw = bool;
|
||||
type RawEmail<'a> = &'a str;
|
||||
type TextMime<'a> = &'a str;
|
||||
|
||||
pub(crate) type All = bool;
|
||||
pub(crate) type Attachments<'a> = Vec<&'a str>;
|
||||
pub(crate) type Headers<'a> = Vec<&'a str>;
|
||||
pub(crate) type Id<'a> = &'a str;
|
||||
pub(crate) type Ids<'a> = &'a str;
|
||||
pub type All = bool;
|
||||
pub type Criteria = String;
|
||||
pub type Folder<'a> = &'a str;
|
||||
pub type Headers<'a> = Vec<&'a str>;
|
||||
pub type Id<'a> = &'a str;
|
||||
pub type Ids<'a> = Vec<&'a str>;
|
||||
pub type Page = usize;
|
||||
pub type PageSize = usize;
|
||||
pub type Query = String;
|
||||
pub type Raw = bool;
|
||||
pub type RawEmail = String;
|
||||
pub type Sanitize = bool;
|
||||
pub type TextMime<'a> = &'a str;
|
||||
|
||||
/// Represents the email commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Attachments(Id<'a>),
|
||||
Copy(Id<'a>, Folder<'a>),
|
||||
Attachments(Ids<'a>),
|
||||
Copy(Ids<'a>, Folder<'a>),
|
||||
Delete(Ids<'a>),
|
||||
Forward(Id<'a>, Attachments<'a>, Encrypt),
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
List(table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Move(Id<'a>, Folder<'a>),
|
||||
Read(Id<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>),
|
||||
Reply(Id<'a>, All, Attachments<'a>, Encrypt),
|
||||
Save(RawEmail<'a>),
|
||||
Move(Ids<'a>, Folder<'a>),
|
||||
Read(Ids<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>),
|
||||
Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
Save(RawEmail),
|
||||
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
|
||||
Send(RawEmail<'a>),
|
||||
Send(RawEmail),
|
||||
Sort(
|
||||
Criteria,
|
||||
Query,
|
||||
|
@ -74,74 +69,61 @@ pub enum Cmd<'a> {
|
|||
Option<PageSize>,
|
||||
Page,
|
||||
),
|
||||
Write(TplOverride<'a>, Attachments<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
||||
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
|
||||
}
|
||||
|
||||
/// Email command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
trace!("matches: {:?}", m);
|
||||
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
|
||||
debug!("attachments command matched");
|
||||
let id = parse_id_arg(m);
|
||||
Cmd::Attachments(id)
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Attachments(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
|
||||
debug!("copy command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Copy(id, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
|
||||
debug!("delete command matched");
|
||||
Cmd::Copy(ids, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
|
||||
let ids = parse_ids_arg(m);
|
||||
Cmd::Delete(ids)
|
||||
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
|
||||
Cmd::Flag(flag::args::matches(m)?)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
debug!("forward command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
Cmd::Forward(id, attachments, encrypt)
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Forward(id, headers, body)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
|
||||
debug!("list command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
Cmd::List(max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
|
||||
debug!("move command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let ids = parse_ids_arg(m);
|
||||
let folder = folder::args::parse_target_arg(m);
|
||||
Cmd::Move(id, folder)
|
||||
Cmd::Move(ids, folder)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
|
||||
debug!("read command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let ids = parse_ids_arg(m);
|
||||
let mime = parse_mime_type_arg(m);
|
||||
let sanitize = parse_sanitize_flag(m);
|
||||
let raw = parse_raw_flag(m);
|
||||
let headers = parse_headers_arg(m);
|
||||
Cmd::Read(id, mime, sanitize, raw, headers)
|
||||
Cmd::Read(ids, mime, sanitize, raw, headers)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
debug!("reply command matched");
|
||||
let id = parse_id_arg(m);
|
||||
let all = parse_reply_all_flag(m);
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
Cmd::Reply(id, all, attachments, encrypt)
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Reply(id, all, headers, body)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
debug!("save command matched");
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Save(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
|
||||
debug!("search command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
let query = parse_query_arg(m);
|
||||
Cmd::Search(query, max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
|
||||
debug!("sort command matched");
|
||||
let max_table_width = table::args::parse_max_width(m);
|
||||
let page_size = parse_page_size_arg(m);
|
||||
let page = parse_page_arg(m);
|
||||
|
@ -149,21 +131,15 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
let query = parse_query_arg(m);
|
||||
Cmd::Sort(criteria, query, max_table_width, page_size, page)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
debug!("send command matched");
|
||||
let email = parse_raw_arg(m);
|
||||
Cmd::Send(email)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
debug!("write command matched");
|
||||
let attachments = parse_attachments_arg(m);
|
||||
let encrypt = parse_encrypt_flag(m);
|
||||
let tpl = tpl::args::parse_override_arg(m);
|
||||
Cmd::Write(tpl, attachments, encrypt)
|
||||
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
|
||||
Cmd::Tpl(tpl::args::matches(m)?)
|
||||
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
|
||||
Cmd::Flag(flag::args::matches(m)?)
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
let headers = tpl::args::parse_headers_arg(m);
|
||||
let body = tpl::args::parse_body_arg(m);
|
||||
Cmd::Write(headers, body)
|
||||
} else {
|
||||
debug!("default list command matched");
|
||||
Cmd::List(None, None, 0)
|
||||
};
|
||||
|
||||
|
@ -171,80 +147,74 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
}
|
||||
|
||||
/// Represents the email subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
pub fn subcmds() -> Vec<Command> {
|
||||
vec![
|
||||
flag::args::subcmds(),
|
||||
tpl::args::subcmds(),
|
||||
vec![
|
||||
SubCommand::with_name(CMD_ATTACHMENTS)
|
||||
.aliases(&["attachment", "attach", "att", "at", "a"])
|
||||
.about("Downloads all attachments of the targeted email")
|
||||
.arg(email::args::id_arg()),
|
||||
SubCommand::with_name(CMD_LIST)
|
||||
.aliases(&["lst", "l"])
|
||||
.about("Lists all emails")
|
||||
Command::new(CMD_ATTACHMENTS)
|
||||
.about("Downloads all emails attachments")
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_LIST)
|
||||
.alias("lst")
|
||||
.about("List envelopes")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width()),
|
||||
SubCommand::with_name(CMD_SEARCH)
|
||||
.aliases(&["s", "query", "q"])
|
||||
.about("Lists emails matching the given query")
|
||||
Command::new(CMD_SEARCH)
|
||||
.aliases(["query", "q"])
|
||||
.about("Filter envelopes matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(query_arg()),
|
||||
SubCommand::with_name(CMD_SORT)
|
||||
.about("Sorts emails by the given criteria and matching the given query")
|
||||
Command::new(CMD_SORT)
|
||||
.about("Sort envelopes by the given criteria and matching the given query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table::args::max_width())
|
||||
.arg(criteria_arg())
|
||||
.arg(query_arg()),
|
||||
SubCommand::with_name(CMD_WRITE)
|
||||
.about("Writes a new email")
|
||||
.aliases(&["w", "new", "n"])
|
||||
.args(&tpl::args::args())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_SEND)
|
||||
.about("Sends a raw email")
|
||||
Command::new(CMD_WRITE)
|
||||
.about("Write a new email")
|
||||
.aliases(["new", "n"])
|
||||
.args(tpl::args::args()),
|
||||
Command::new(CMD_SEND)
|
||||
.about("Send a raw email")
|
||||
.arg(raw_arg()),
|
||||
SubCommand::with_name(CMD_SAVE)
|
||||
.about("Saves a raw email")
|
||||
Command::new(CMD_SAVE)
|
||||
.about("Save a raw email")
|
||||
.arg(raw_arg()),
|
||||
SubCommand::with_name(CMD_READ)
|
||||
.about("Reads text bodies of an email")
|
||||
.arg(id_arg())
|
||||
Command::new(CMD_READ)
|
||||
.about("Read text bodies of emails")
|
||||
.arg(mime_type_arg())
|
||||
.arg(sanitize_flag())
|
||||
.arg(raw_flag())
|
||||
.arg(headers_arg()),
|
||||
SubCommand::with_name(CMD_REPLY)
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Answers to an email")
|
||||
.arg(id_arg())
|
||||
.arg(headers_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_REPLY)
|
||||
.about("Answer to an email")
|
||||
.arg(reply_all_flag())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_FORWARD)
|
||||
.aliases(&["fwd", "f"])
|
||||
.about("Forwards an email")
|
||||
.arg(id_arg())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_flag()),
|
||||
SubCommand::with_name(CMD_COPY)
|
||||
.aliases(&["cp", "c"])
|
||||
.about("Copies an email to the targeted folder")
|
||||
.arg(id_arg())
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name(CMD_MOVE)
|
||||
.aliases(&["mv"])
|
||||
.about("Moves an email to the targeted folder")
|
||||
.arg(id_arg())
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name(CMD_DEL)
|
||||
.aliases(&["del", "d", "remove", "rm"])
|
||||
.about("Deletes an email")
|
||||
.args(tpl::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_FORWARD)
|
||||
.aliases(["fwd", "f"])
|
||||
.about("Forward an email")
|
||||
.args(tpl::args::args())
|
||||
.arg(id_arg()),
|
||||
Command::new(CMD_COPY)
|
||||
.alias("cp")
|
||||
.about("Copy emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_MOVE)
|
||||
.alias("mv")
|
||||
.about("Move emails to the given folder")
|
||||
.arg(folder::args::target_arg())
|
||||
.arg(ids_arg()),
|
||||
Command::new(CMD_DELETE)
|
||||
.aliases(["remove", "rm"])
|
||||
.about("Delete emails")
|
||||
.arg(ids_arg()),
|
||||
],
|
||||
]
|
||||
|
@ -252,29 +222,45 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
}
|
||||
|
||||
/// Represents the email id argument.
|
||||
pub fn id_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ID)
|
||||
pub fn id_arg() -> Arg {
|
||||
Arg::new(ARG_ID)
|
||||
.help("Specifies the target email")
|
||||
.value_name("ID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email id argument parser.
|
||||
pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_ID).unwrap()
|
||||
pub fn parse_id_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_ID).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email ids argument.
|
||||
pub fn ids_arg() -> Arg {
|
||||
Arg::new(ARG_IDS)
|
||||
.help("Email ids")
|
||||
.value_name("IDS")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email ids argument parser.
|
||||
pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> {
|
||||
matches
|
||||
.get_many::<String>(ARG_IDS)
|
||||
.unwrap()
|
||||
.map(String::as_str)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the email sort criteria argument.
|
||||
pub fn criteria_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_CRITERIA)
|
||||
.long("criterion")
|
||||
.short("c")
|
||||
pub fn criteria_arg<'a>() -> Arg {
|
||||
Arg::new(ARG_CRITERIA)
|
||||
.help("Email sorting preferences")
|
||||
.long("criterion")
|
||||
.short('c')
|
||||
.value_name("CRITERION:ORDER")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
.possible_values(&[
|
||||
.action(ArgAction::Append)
|
||||
.value_parser([
|
||||
"arrival",
|
||||
"arrival:asc",
|
||||
"arrival:desc",
|
||||
|
@ -300,69 +286,59 @@ pub fn criteria_arg<'a>() -> Arg<'a, 'a> {
|
|||
}
|
||||
|
||||
/// Represents the email sort criteria argument parser.
|
||||
pub fn parse_criteria_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
|
||||
matches
|
||||
.values_of(ARG_CRITERIA)
|
||||
.get_many::<String>(ARG_CRITERIA)
|
||||
.unwrap_or_default()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
/// Represents the email ids argument.
|
||||
pub fn ids_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_IDS)
|
||||
.help("Specifies the target email(s)")
|
||||
.long_help("Specifies a range of emails. The range follows the RFC3501 format.")
|
||||
.value_name("IDS")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email ids argument parser.
|
||||
pub fn parse_ids_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(email::args::ARG_IDS).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument.
|
||||
pub fn reply_all_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_REPLY_ALL)
|
||||
pub fn reply_all_flag() -> Arg {
|
||||
Arg::new(ARG_REPLY_ALL)
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
.short('a')
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the email reply all argument parser.
|
||||
pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_REPLY_ALL)
|
||||
pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
|
||||
matches.get_flag(ARG_REPLY_ALL)
|
||||
}
|
||||
|
||||
/// Represents the page size argument.
|
||||
fn page_size_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_PAGE_SIZE)
|
||||
fn page_size_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE_SIZE)
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.long("page-size")
|
||||
.short('s')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the page size argument parser.
|
||||
fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<usize> {
|
||||
matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok())
|
||||
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.get_one::<String>(ARG_PAGE_SIZE)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
||||
/// Represents the page argument.
|
||||
fn page_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_PAGE)
|
||||
fn page_arg() -> Arg {
|
||||
Arg::new(ARG_PAGE)
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.short('p')
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
/// Represents the page argument parser.
|
||||
fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize {
|
||||
fn parse_page_arg(matches: &ArgMatches) -> usize {
|
||||
matches
|
||||
.value_of(ARG_PAGE)
|
||||
.get_one::<String>(ARG_PAGE)
|
||||
.unwrap()
|
||||
.parse()
|
||||
.ok()
|
||||
|
@ -370,120 +346,94 @@ fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email attachments argument.
|
||||
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ATTACHMENTS)
|
||||
.help("Adds attachment to the email")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
/// Represents the email attachments argument parser.
|
||||
pub fn parse_attachments_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
|
||||
matches
|
||||
.values_of(ARG_ATTACHMENTS)
|
||||
.unwrap_or_default()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Represents the email headers argument.
|
||||
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_HEADERS)
|
||||
pub fn headers_arg() -> Arg {
|
||||
Arg::new(ARG_HEADERS)
|
||||
.help("Shows additional headers with the email")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.short('H')
|
||||
.value_name("STRING")
|
||||
.multiple(true)
|
||||
.action(ArgAction::Append)
|
||||
}
|
||||
|
||||
/// Represents the email headers argument parser.
|
||||
pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
|
||||
matches.values_of(ARG_HEADERS).unwrap_or_default().collect()
|
||||
pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
|
||||
m.get_many::<String>(ARG_HEADERS)
|
||||
.unwrap_or_default()
|
||||
.map(String::as_str)
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// Represents the sanitize flag.
|
||||
pub fn sanitize_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_SANITIZE)
|
||||
pub fn sanitize_flag() -> Arg {
|
||||
Arg::new(ARG_SANITIZE)
|
||||
.help("Sanitizes text bodies")
|
||||
.long("sanitize")
|
||||
.short("s")
|
||||
.short('s')
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the raw flag.
|
||||
pub fn raw_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_RAW)
|
||||
pub fn raw_flag() -> Arg {
|
||||
Arg::new(ARG_RAW)
|
||||
.help("Returns raw version of email")
|
||||
.long("raw")
|
||||
.short("r")
|
||||
.short('r')
|
||||
.action(ArgAction::SetTrue)
|
||||
}
|
||||
|
||||
/// Represents the sanitize flag parser.
|
||||
pub fn parse_sanitize_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_SANITIZE)
|
||||
pub fn parse_sanitize_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_SANITIZE)
|
||||
}
|
||||
|
||||
/// Represents the raw flag parser.
|
||||
pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_RAW)
|
||||
pub fn parse_raw_flag(m: &ArgMatches) -> bool {
|
||||
m.get_flag(ARG_RAW)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument.
|
||||
pub fn raw_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_RAW).raw(true)
|
||||
pub fn raw_arg() -> Arg {
|
||||
Arg::new(ARG_RAW).raw(true)
|
||||
}
|
||||
|
||||
/// Represents the email raw argument parser.
|
||||
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_RAW).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email encrypt flag.
|
||||
pub fn encrypt_flag<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_ENCRYPT)
|
||||
.help("Encrypts the email")
|
||||
.short("e")
|
||||
.long("encrypt")
|
||||
}
|
||||
|
||||
/// Represents the email encrypt flag parser.
|
||||
pub fn parse_encrypt_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
|
||||
matches.is_present(ARG_ENCRYPT)
|
||||
pub fn parse_raw_arg(m: &ArgMatches) -> String {
|
||||
m.get_one::<String>(ARG_RAW).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument.
|
||||
pub fn mime_type_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_MIME_TYPE)
|
||||
pub fn mime_type_arg() -> Arg {
|
||||
Arg::new(ARG_MIME_TYPE)
|
||||
.help("MIME type to use")
|
||||
.short("t")
|
||||
.short('t')
|
||||
.long("mime-type")
|
||||
.value_name("MIME")
|
||||
.possible_values(&["plain", "html"])
|
||||
.value_parser(["plain", "html"])
|
||||
.default_value("plain")
|
||||
}
|
||||
|
||||
/// Represents the email MIME type argument parser.
|
||||
pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_MIME_TYPE).unwrap()
|
||||
pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
|
||||
}
|
||||
|
||||
/// Represents the email query argument.
|
||||
pub fn query_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_QUERY)
|
||||
pub fn query_arg() -> Arg {
|
||||
Arg::new(ARG_QUERY)
|
||||
.long_help("The query system depends on the backend, see the wiki for more details")
|
||||
.value_name("QUERY")
|
||||
.multiple(true)
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the email query argument parser.
|
||||
pub fn parse_query_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
pub fn parse_query_arg(matches: &ArgMatches) -> String {
|
||||
matches
|
||||
.values_of(ARG_QUERY)
|
||||
.get_many::<String>(ARG_QUERY)
|
||||
.unwrap_or_default()
|
||||
.fold((false, vec![]), |(escape, mut cmds), cmd| {
|
||||
match (cmd, escape) {
|
||||
match (cmd.as_str(), escape) {
|
||||
// Next command is an arg and needs to be escaped
|
||||
("subject", _) | ("body", _) | ("text", _) => {
|
||||
cmds.push(cmd.to_string());
|
||||
|
|
|
@ -1,123 +1,138 @@
|
|||
//! Module related to message handling.
|
||||
//!
|
||||
//! This module gathers all message commands.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{
|
||||
AccountConfig, Backend, Email, Part, Parts, PartsReaderOptions, Sender, TextPlainPart,
|
||||
TplOverride,
|
||||
AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder,
|
||||
};
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
use log::{debug, trace};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
};
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
pub fn attachments<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
let attachments = backend.email_get(mbox, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let emails = backend.get_emails(&folder, ids.clone())?;
|
||||
let mut index = 0;
|
||||
|
||||
if attachments_len == 0 {
|
||||
return printer.print_struct(format!("No attachment found for message {}", seq));
|
||||
let mut emails_count = 0;
|
||||
let mut attachments_count = 0;
|
||||
|
||||
for email in emails.to_vec() {
|
||||
let id = ids.get(index).unwrap();
|
||||
let attachments = email.attachments()?;
|
||||
|
||||
index = index + 1;
|
||||
|
||||
if attachments.is_empty() {
|
||||
printer.print_log(format!("No attachment found for email #{}", id))?;
|
||||
continue;
|
||||
} else {
|
||||
emails_count = emails_count + 1;
|
||||
}
|
||||
|
||||
printer.print_log(format!(
|
||||
"{} attachment(s) found for email #{}…",
|
||||
attachments.len(),
|
||||
id
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let filename = attachment
|
||||
.filename
|
||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||
let filepath = config.get_download_file_path(&filename)?;
|
||||
printer.print_log(format!("Downloading {:?}…", filepath))?;
|
||||
fs::write(&filepath, &attachment.body).context("cannot download attachment")?;
|
||||
attachments_count = attachments_count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
printer.print_str(format!(
|
||||
"{} attachment(s) found for message {}",
|
||||
attachments_len, seq
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
let file_path = config.get_download_file_path(&attachment.filename)?;
|
||||
printer.print_str(format!("Downloading {:?}…", file_path))?;
|
||||
fs::write(&file_path, &attachment.content)
|
||||
.context(format!("cannot download attachment {:?}", file_path))?;
|
||||
match attachments_count {
|
||||
0 => printer.print("No attachment found!"),
|
||||
1 => printer.print("Downloaded 1 attachment!"),
|
||||
n => printer.print(format!(
|
||||
"Downloaded {} attachment(s) from {} email(s)!",
|
||||
n, emails_count,
|
||||
)),
|
||||
}
|
||||
|
||||
printer.print_struct("Done!")
|
||||
}
|
||||
|
||||
/// Copy a message from a folder to another.
|
||||
pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
pub fn copy<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
backend.email_copy(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
"Message {} successfully copied to folder {}",
|
||||
seq, mbox_dst
|
||||
))
|
||||
let from_folder = config.folder_alias(from_folder)?;
|
||||
let to_folder = config.folder_alias(to_folder)?;
|
||||
backend.copy_emails(&from_folder, &to_folder, ids)?;
|
||||
printer.print("Email(s) successfully copied!")
|
||||
}
|
||||
|
||||
/// Delete messages matching the given sequence range.
|
||||
pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
pub fn delete<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
backend.email_delete(mbox, seq)?;
|
||||
printer.print_struct(format!("Message(s) {} successfully deleted", seq))
|
||||
let folder = config.folder_alias(folder)?;
|
||||
backend.delete_emails(&folder, ids)?;
|
||||
printer.print("Email(s) successfully deleted!")
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected folder.
|
||||
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
pub fn forward<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_email_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let tpl = backend
|
||||
.get_emails(&folder, vec![id])?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_forward_tpl_builder(config)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List paginated messages from the selected folder.
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
pub fn list<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_list(mbox, page_size, page)?;
|
||||
let msgs = backend.list_envelopes(&folder, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
|
@ -131,242 +146,125 @@ pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
|||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
url: &Url,
|
||||
pub fn mailto<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
url: &Url,
|
||||
) -> Result<()> {
|
||||
info!("entering mailto command handler");
|
||||
|
||||
let to = addrparse(url.path())?;
|
||||
let mut cc = Vec::new();
|
||||
let mut bcc = Vec::new();
|
||||
let mut subject = Cow::default();
|
||||
let mut body = Cow::default();
|
||||
let mut tpl = TplBuilder::default().to(url.path());
|
||||
|
||||
for (key, val) in url.query_pairs() {
|
||||
match key.as_bytes() {
|
||||
b"cc" => {
|
||||
cc.push(val.to_string());
|
||||
}
|
||||
b"bcc" => {
|
||||
bcc.push(val.to_string());
|
||||
}
|
||||
b"subject" => {
|
||||
subject = val;
|
||||
}
|
||||
b"body" => {
|
||||
body = val;
|
||||
}
|
||||
match key.to_lowercase().as_bytes() {
|
||||
b"cc" => tpl = tpl.cc(val),
|
||||
b"bcc" => tpl = tpl.bcc(val),
|
||||
b"subject" => tpl = tpl.subject(val),
|
||||
b"body" => tpl = tpl.text_plain_part(val.as_bytes()),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let msg = Email {
|
||||
from: Some(vec![config.address()?].into()),
|
||||
to: if to.is_empty() { None } else { Some(to) },
|
||||
cc: if cc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&cc.join(","))?)
|
||||
},
|
||||
bcc: if bcc.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(addrparse(&bcc.join(","))?)
|
||||
},
|
||||
subject: subject.into(),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
})]),
|
||||
..Email::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
editor::edit_email_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
Ok(())
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl.build())
|
||||
}
|
||||
|
||||
/// Move a message from a folder to another.
|
||||
pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
pub fn move_<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
from_folder: &str,
|
||||
to_folder: &str,
|
||||
ids: Vec<&str>,
|
||||
) -> Result<()> {
|
||||
backend.email_move(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully moved to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
))
|
||||
let from_folder = config.folder_alias(from_folder)?;
|
||||
let to_folder = config.folder_alias(to_folder)?;
|
||||
backend.move_emails(&from_folder, &to_folder, ids)?;
|
||||
printer.print("Email(s) successfully moved!")
|
||||
}
|
||||
|
||||
/// Read a message by its sequence number.
|
||||
pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
pub fn read<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
text_mime: &str,
|
||||
sanitize: bool,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
) -> Result<()> {
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let emails = backend.get_emails(&folder, ids)?;
|
||||
|
||||
let mut glue = "";
|
||||
let mut bodies = String::default();
|
||||
|
||||
for email in emails.to_vec() {
|
||||
bodies.push_str(glue);
|
||||
|
||||
if raw {
|
||||
// emails do not always have valid utf8, uses "lossy" to
|
||||
// display what can be displayed
|
||||
bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned());
|
||||
} else {
|
||||
let tpl = email
|
||||
.to_read_tpl_builder(config)?
|
||||
.show_headers(config.email_reading_headers())
|
||||
.show_headers(&headers)
|
||||
.show_text_parts_only(true)
|
||||
.use_show_text_parts_strategy(if text_mime == "plain" {
|
||||
ShowTextPartsStrategy::PlainOtherwiseHtml
|
||||
} else {
|
||||
ShowTextPartsStrategy::HtmlOtherwisePlain
|
||||
})
|
||||
.sanitize_text_parts(sanitize)
|
||||
.build();
|
||||
|
||||
bodies.push_str(&<Tpl as Into<String>>::into(tpl));
|
||||
}
|
||||
|
||||
glue = "\n\n";
|
||||
}
|
||||
|
||||
printer.print(bodies)
|
||||
}
|
||||
|
||||
pub fn reply<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let msg = backend.email_get(mbox, seq)?;
|
||||
|
||||
printer.print_struct(if raw {
|
||||
// Emails do not always have valid utf8. Using "lossy" to
|
||||
// display what we can.
|
||||
String::from_utf8_lossy(&msg.raw).into_owned()
|
||||
} else {
|
||||
msg.to_readable(
|
||||
config,
|
||||
PartsReaderOptions {
|
||||
plain_first: text_mime == "plain",
|
||||
sanitize,
|
||||
},
|
||||
headers,
|
||||
)?
|
||||
})
|
||||
}
|
||||
|
||||
/// Reply to the given message UID.
|
||||
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
sender: &mut S,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
all: bool,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_email_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
backend.flags_add(mbox, seq, "replied")?;
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let tpl = backend
|
||||
.get_emails(&folder, vec![id])?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_reply_tpl_builder(config, all)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?;
|
||||
backend.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted folder.
|
||||
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
raw_msg: &str,
|
||||
pub fn save<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
raw_email: String,
|
||||
) -> Result<()> {
|
||||
debug!("folder: {}", mbox);
|
||||
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
raw_msg.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
backend.email_add(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paginate messages from the selected folder matching the specified
|
||||
/// query.
|
||||
pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_search(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Paginates messages from the selected folder matching the specified
|
||||
/// query, sorted by the given criteria.
|
||||
pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.envelope_search(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a raw message.
|
||||
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
raw_email: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering send message handler");
|
||||
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
debug!("is tty: {}", is_tty);
|
||||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let sent_folder = config.folder_alias("sent")?;
|
||||
debug!("sent folder: {:?}", sent_folder);
|
||||
|
||||
let raw_email = if is_tty || is_json {
|
||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
|
@ -377,26 +275,92 @@ pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
|||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
trace!("raw message: {:?}", raw_email);
|
||||
let email = Email::from_tpl(&raw_email)?;
|
||||
sender.send(&email)?;
|
||||
backend.email_add(&sent_folder, raw_email.as_bytes(), "seen")?;
|
||||
backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
tpl: TplOverride,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
pub fn search<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
let envelopes = backend.search_envelopes(&folder, &query, "", page_size, page)?;
|
||||
let opts = PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
};
|
||||
|
||||
printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub fn sort<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
) -> Result<()> {
|
||||
let folder = config.folder_alias(folder)?;
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
let envelopes = backend.search_envelopes(&folder, &query, &sort, page_size, page)?;
|
||||
let opts = PrintTableOpts {
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
};
|
||||
|
||||
printer.print_table(Box::new(envelopes), opts)
|
||||
}
|
||||
|
||||
pub fn send<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
raw_email: String,
|
||||
) -> Result<()> {
|
||||
let email = Email::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_email_with_editor(email, tpl, config, printer, backend, sender)?;
|
||||
let folder = config.folder_alias("sent")?;
|
||||
let is_tty = atty::is(Stream::Stdin);
|
||||
let is_json = printer.is_json();
|
||||
let raw_email = if is_tty || is_json {
|
||||
raw_email.replace("\r", "").replace("\n", "\r\n")
|
||||
} else {
|
||||
io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
trace!("raw email: {:?}", raw_email);
|
||||
sender.send(raw_email.as_bytes())?;
|
||||
backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl = Email::new_tpl_builder(config)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
trace!("initial template: {}", *tpl);
|
||||
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ impl Table for Envelope {
|
|||
.cell(Cell::new("ID").bold().underline().white())
|
||||
.cell(Cell::new("FLAGS").bold().underline().white())
|
||||
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("SENDER").bold().underline().white())
|
||||
.cell(Cell::new("FROM").bold().underline().white())
|
||||
.cell(Cell::new("DATE").bold().underline().white())
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,12 @@ impl Table for Envelope {
|
|||
let flags = self.flags.to_symbols_string();
|
||||
let unseen = !self.flags.contains(&Flag::Seen);
|
||||
let subject = &self.subject;
|
||||
let sender = &self.sender;
|
||||
let date = self.date.as_deref().unwrap_or_default();
|
||||
let sender = if let Some(name) = &self.from.name {
|
||||
name
|
||||
} else {
|
||||
&self.from.addr
|
||||
};
|
||||
let date = self.date.format("%d/%m/%Y %H:%M").to_string();
|
||||
|
||||
Row::new()
|
||||
.cell(Cell::new(id).bold_if(unseen).red())
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
//! Email flag CLI module.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher
|
||||
//! related to the email flag domain.
|
||||
//! This module contains the command matcher, the subcommands and the
|
||||
//! arguments related to the email flag domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use himalaya_lib::{Flag, Flags};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::email;
|
||||
|
@ -12,38 +13,36 @@ use crate::email;
|
|||
const ARG_FLAGS: &str = "flag";
|
||||
|
||||
const CMD_ADD: &str = "add";
|
||||
const CMD_DEL: &str = "remove";
|
||||
const CMD_REMOVE: &str = "remove";
|
||||
const CMD_SET: &str = "set";
|
||||
|
||||
pub(crate) const CMD_FLAG: &str = "flag";
|
||||
|
||||
type Flags = String;
|
||||
pub(crate) const CMD_FLAG: &str = "flags";
|
||||
|
||||
/// Represents the flag commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Add(email::args::Ids<'a>, Flags),
|
||||
Remove(email::args::Ids<'a>, Flags),
|
||||
Set(email::args::Ids<'a>, Flags),
|
||||
Del(email::args::Ids<'a>, Flags),
|
||||
}
|
||||
|
||||
/// Represents the flag command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
|
||||
debug!("add subcommand matched");
|
||||
debug!("add flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Add(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REMOVE) {
|
||||
info!("remove flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Remove(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
|
||||
debug!("set subcommand matched");
|
||||
debug!("set flags command matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
let flags = parse_flags_arg(m);
|
||||
Some(Cmd::Set(ids, flags))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
|
||||
info!("remove subcommand matched");
|
||||
let ids = email::args::parse_ids_arg(m);
|
||||
let flags: String = parse_flags_arg(m);
|
||||
Some(Cmd::Del(ids, flags))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -52,48 +51,51 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
}
|
||||
|
||||
/// Represents the flag subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_FLAG)
|
||||
.aliases(&["flags", "flg"])
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_FLAG)
|
||||
.about("Handles email flags")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_ADD)
|
||||
.aliases(&["a"])
|
||||
.about("Adds email flags")
|
||||
Command::new(CMD_ADD)
|
||||
.about("Adds flags to an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SET)
|
||||
.aliases(&["s", "change", "c"])
|
||||
.about("Sets email flags")
|
||||
Command::new(CMD_REMOVE)
|
||||
.aliases(["delete", "del", "d"])
|
||||
.about("Removes flags from an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_DEL)
|
||||
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
|
||||
.about("Removes email flags")
|
||||
Command::new(CMD_SET)
|
||||
.aliases(["change", "c"])
|
||||
.about("Sets flags of an email")
|
||||
.arg(email::args::ids_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the flags argument.
|
||||
pub fn flags_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_FLAGS)
|
||||
.long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
|
||||
.value_name("FLAGS…")
|
||||
.multiple(true)
|
||||
pub fn flags_arg() -> Arg {
|
||||
Arg::new(ARG_FLAGS)
|
||||
.value_name("FLAGS")
|
||||
.help("The flags")
|
||||
.long_help("The list of flags. It can be one of: seen, answered, flagged, deleted, draft, recent. Other flags are considered custom.")
|
||||
.num_args(1..)
|
||||
.required(true)
|
||||
.last(true)
|
||||
}
|
||||
|
||||
/// Represents the flags argument parser.
|
||||
pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
|
||||
matches
|
||||
.values_of(ARG_FLAGS)
|
||||
.unwrap_or_default()
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
pub fn parse_flags_arg(matches: &ArgMatches) -> Flags {
|
||||
Flags::from_iter(
|
||||
matches
|
||||
.get_many::<String>(ARG_FLAGS)
|
||||
.unwrap_or_default()
|
||||
.map(String::as_str)
|
||||
.map(Flag::from),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,56 +1,37 @@
|
|||
//! Message flag handling module.
|
||||
//!
|
||||
//! This module gathers all flag actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::backend::Backend;
|
||||
use himalaya_lib::{Backend, Flags};
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Adds flags to all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn add<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
pub fn add<P: Printer, B: Backend + ?Sized>(
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
backend.flags_add(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully added to message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
backend.add_flags(folder, ids, flags)?;
|
||||
printer.print("Flag(s) successfully added!")
|
||||
}
|
||||
|
||||
/// Removes flags from all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn remove<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
pub fn set<P: Printer, B: Backend + ?Sized>(
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
backend.flags_delete(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully removed from message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
backend.set_flags(folder, ids, flags)?;
|
||||
printer.print("Flag(s) successfully set!")
|
||||
}
|
||||
|
||||
/// Replaces flags of all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn set<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
pub fn remove<P: Printer, B: Backend + ?Sized>(
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
ids: Vec<&str>,
|
||||
flags: &Flags,
|
||||
) -> Result<()> {
|
||||
backend.flags_set(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
||||
))
|
||||
backend.remove_flags(folder, ids, flags)?;
|
||||
printer.print("Flag(s) successfully removed!")
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
//! related to the folder domain.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use clap::{self, Arg, ArgMatches, Command};
|
||||
use log::debug;
|
||||
|
||||
use crate::ui::table;
|
||||
|
@ -32,122 +32,108 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
|||
Ok(cmd)
|
||||
}
|
||||
|
||||
/// Represents folder subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_FOLDERS)
|
||||
.aliases(&[
|
||||
"folder",
|
||||
"fold",
|
||||
"fo",
|
||||
"mailboxes",
|
||||
"mailbox",
|
||||
"mboxes",
|
||||
"mbox",
|
||||
"mb",
|
||||
"m",
|
||||
])
|
||||
/// Represents the folder subcommand.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_FOLDERS)
|
||||
.about("Lists folders")
|
||||
.arg(table::args::max_width())]
|
||||
.arg(table::args::max_width())
|
||||
}
|
||||
|
||||
/// Represents the source folder argument.
|
||||
pub fn source_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_SOURCE)
|
||||
.short("f")
|
||||
pub fn source_arg() -> Arg {
|
||||
Arg::new(ARG_SOURCE)
|
||||
.long("folder")
|
||||
.short('f')
|
||||
.help("Specifies the source folder")
|
||||
.value_name("SOURCE")
|
||||
.default_value("inbox")
|
||||
}
|
||||
|
||||
/// Represents the source folder argument parser.
|
||||
pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_SOURCE).unwrap()
|
||||
pub fn parse_source_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_SOURCE).unwrap().as_str()
|
||||
}
|
||||
|
||||
/// Represents the target folder argument.
|
||||
pub fn target_arg<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_TARGET)
|
||||
pub fn target_arg() -> Arg {
|
||||
Arg::new(ARG_TARGET)
|
||||
.help("Specifies the target folder")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
/// Represents the target folder argument parser.
|
||||
pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_TARGET).unwrap()
|
||||
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
|
||||
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use clap::{App, ErrorKind};
|
||||
use clap::{error::ErrorKind, Command};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_should_match_cmds() {
|
||||
let arg = App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders"]);
|
||||
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
|
||||
|
||||
let arg = App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
let arg = Command::new("himalaya")
|
||||
.subcommand(subcmd())
|
||||
.get_matches_from(&["himalaya", "folders", "--max-width", "20"]);
|
||||
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_aliases() {
|
||||
macro_rules! get_matches_from {
|
||||
($alias:expr) => {
|
||||
App::new("himalaya")
|
||||
.subcommands(subcmds())
|
||||
.get_matches_from(&["himalaya", $alias])
|
||||
.subcommand_name()
|
||||
};
|
||||
}
|
||||
|
||||
assert_eq!(Some("folders"), get_matches_from!["folders"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["folder"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["fold"]);
|
||||
assert_eq!(Some("folders"), get_matches_from!["fo"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_source_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
App::new("himalaya")
|
||||
Command::new("himalaya")
|
||||
.arg(source_arg())
|
||||
.get_matches_from(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(Some("inbox"), app.value_of("source"));
|
||||
assert_eq!(
|
||||
Some("inbox"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
|
||||
let app = get_matches_from!["-f", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("source"));
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
|
||||
let app = get_matches_from!["--folder", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("source"));
|
||||
assert_eq!(
|
||||
Some("SOURCE"),
|
||||
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_should_match_target_arg() {
|
||||
macro_rules! get_matches_from {
|
||||
($($arg:expr),*) => {
|
||||
App::new("himalaya")
|
||||
Command::new("himalaya")
|
||||
.arg(target_arg())
|
||||
.get_matches_from_safe(&["himalaya", $($arg,)*])
|
||||
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
|
||||
};
|
||||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind);
|
||||
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
|
||||
|
||||
let app = get_matches_from!["TARGET"];
|
||||
assert_eq!(Some("TARGET"), app.unwrap().value_of("target"));
|
||||
assert_eq!(
|
||||
Some("TARGET"),
|
||||
app.unwrap()
|
||||
.get_one::<String>(ARG_TARGET)
|
||||
.map(String::as_str)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,19 +4,16 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::{AccountConfig, Backend};
|
||||
use log::trace;
|
||||
|
||||
use crate::printer::{PrintTableOpts, Printer};
|
||||
|
||||
/// Lists all folders.
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
pub fn list<P: Printer, B: Backend + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let folders = backend.folder_list()?;
|
||||
trace!("folders: {:?}", folders);
|
||||
let folders = backend.list_folders()?;
|
||||
printer.print_table(
|
||||
// TODO: remove Box
|
||||
Box::new(folders),
|
||||
|
@ -29,8 +26,10 @@ pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders};
|
||||
use std::{fmt::Debug, io};
|
||||
use himalaya_lib::{
|
||||
backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders,
|
||||
};
|
||||
use std::{any::Any, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
@ -87,10 +86,10 @@ mod tests {
|
|||
data.print_table(&mut self.writer, opts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn print_str<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(
|
||||
fn print<T: Debug + Print + serde::Serialize>(
|
||||
&mut self,
|
||||
_data: T,
|
||||
) -> anyhow::Result<()> {
|
||||
|
@ -103,12 +102,15 @@ mod tests {
|
|||
|
||||
struct TestBackend;
|
||||
|
||||
impl<'a> Backend<'a> for TestBackend {
|
||||
fn folder_add(&mut self, _: &str) -> backend::Result<()> {
|
||||
impl Backend for TestBackend {
|
||||
fn name(&self) -> String {
|
||||
unimplemented!();
|
||||
}
|
||||
fn folder_list(&mut self) -> backend::Result<Folders> {
|
||||
Ok(Folders(vec![
|
||||
fn add_folder(&self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn list_folders(&self) -> backend::Result<Folders> {
|
||||
Ok(Folders::from_iter([
|
||||
Folder {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
|
@ -121,14 +123,23 @@ mod tests {
|
|||
},
|
||||
]))
|
||||
}
|
||||
fn folder_delete(&mut self, _: &str) -> backend::Result<()> {
|
||||
fn purge_folder(&self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
fn delete_folder(&self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelope(&self, _: &str, _: &str) -> backend::Result<Envelope> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelope_internal(&self, _: &str, _: &str) -> backend::Result<Envelope> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn list_envelopes(&self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn envelope_search(
|
||||
&mut self,
|
||||
fn search_envelopes(
|
||||
&self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
_: &str,
|
||||
|
@ -137,31 +148,63 @@ mod tests {
|
|||
) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
|
||||
fn add_email(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_get(&mut self, _: &str, _: &str) -> backend::Result<Email> {
|
||||
fn add_email_internal(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn get_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn preview_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn get_emails_internal(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn copy_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn copy_emails_internal(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn move_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&self) -> &(dyn std::any::Any + 'a) {
|
||||
fn move_emails_internal(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn delete_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn delete_emails_internal(&self, _: &str, _: Vec<&str>) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_flags_internal(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_flags_internal(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_flags(&self, _: &str, _: Vec<&str>, _: &Flags) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_flags_internal(
|
||||
&self,
|
||||
_: &str,
|
||||
_: Vec<&str>,
|
||||
_: &Flags,
|
||||
) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn as_any(&'static self) -> &(dyn Any) {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,64 +3,60 @@
|
|||
//! This module provides subcommands and a command matcher related to IMAP.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{App, ArgMatches};
|
||||
use log::{debug, info};
|
||||
use clap::{value_parser, Arg, ArgMatches, Command};
|
||||
use log::debug;
|
||||
|
||||
const ARG_KEEPALIVE: &str = "keepalive";
|
||||
const CMD_NOTIFY: &str = "notify";
|
||||
const CMD_WATCH: &str = "watch";
|
||||
|
||||
type Keepalive = u64;
|
||||
|
||||
/// IMAP commands.
|
||||
pub enum Command {
|
||||
pub enum Cmd {
|
||||
/// Start the IMAP notify mode with the give keepalive duration.
|
||||
Notify(Keepalive),
|
||||
|
||||
/// Start the IMAP watch mode with the give keepalive duration.
|
||||
Watch(Keepalive),
|
||||
}
|
||||
|
||||
/// IMAP command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> {
|
||||
info!("entering imap command matcher");
|
||||
|
||||
if let Some(m) = m.subcommand_matches("notify") {
|
||||
info!("notify command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
if let Some(m) = m.subcommand_matches(CMD_NOTIFY) {
|
||||
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Notify(keepalive)));
|
||||
return Ok(Some(Cmd::Notify(*keepalive)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("watch") {
|
||||
info!("watch command matched");
|
||||
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
|
||||
if let Some(m) = m.subcommand_matches(CMD_WATCH) {
|
||||
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
|
||||
debug!("keepalive: {}", keepalive);
|
||||
return Ok(Some(Command::Watch(keepalive)));
|
||||
return Ok(Some(Cmd::Watch(*keepalive)));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// IMAP subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![
|
||||
clap::SubCommand::with_name("notify")
|
||||
Command::new(CMD_NOTIFY)
|
||||
.about("Notifies when new messages arrive in the given folder")
|
||||
.aliases(&["idle"])
|
||||
.arg(
|
||||
clap::Arg::with_name("keepalive")
|
||||
.help("Specifies the keepalive duration")
|
||||
.short("k")
|
||||
.long("keepalive")
|
||||
.value_name("SECS")
|
||||
.default_value("500"),
|
||||
),
|
||||
clap::SubCommand::with_name("watch")
|
||||
.alias("idle")
|
||||
.arg(keepalive_arg()),
|
||||
Command::new(CMD_WATCH)
|
||||
.about("Watches IMAP server changes")
|
||||
.arg(
|
||||
clap::Arg::with_name("keepalive")
|
||||
.help("Specifies the keepalive duration")
|
||||
.short("k")
|
||||
.long("keepalive")
|
||||
.value_name("SECS")
|
||||
.default_value("500"),
|
||||
),
|
||||
.arg(keepalive_arg()),
|
||||
]
|
||||
}
|
||||
|
||||
/// Represents the keepalive argument.
|
||||
pub fn keepalive_arg() -> Arg {
|
||||
Arg::new(ARG_KEEPALIVE)
|
||||
.help("Specifies the keepalive duration.")
|
||||
.long("keepalive")
|
||||
.short('k')
|
||||
.value_name("SECS")
|
||||
.default_value("500")
|
||||
.value_parser(value_parser!(u64))
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::ImapBackend;
|
||||
|
||||
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.notify(keepalive, mbox).context("cannot imap notify")
|
||||
pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
|
||||
imap.notify(keepalive, folder).context("cannot imap notify")
|
||||
}
|
||||
|
||||
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.watch(keepalive, mbox).context("cannot imap watch")
|
||||
pub fn watch(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
|
||||
imap.watch(keepalive, folder).context("cannot imap watch")
|
||||
}
|
||||
|
|
|
@ -4,68 +4,58 @@
|
|||
//! related to email templating.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::email::TplOverride;
|
||||
use log::debug;
|
||||
use clap::{Arg, ArgAction, ArgMatches, Command};
|
||||
|
||||
use crate::email;
|
||||
|
||||
const ARG_BCC: &str = "bcc";
|
||||
const ARG_BODY: &str = "body";
|
||||
const ARG_CC: &str = "cc";
|
||||
const ARG_FROM: &str = "from";
|
||||
const ARG_HEADERS: &str = "header";
|
||||
const ARG_SIGNATURE: &str = "signature";
|
||||
const ARG_SUBJECT: &str = "subject";
|
||||
const ARG_TO: &str = "to";
|
||||
const ARG_HEADERS: &str = "headers";
|
||||
const ARG_TPL: &str = "template";
|
||||
const CMD_FORWARD: &str = "forward";
|
||||
const CMD_NEW: &str = "new";
|
||||
const CMD_REPLY: &str = "reply";
|
||||
const CMD_SAVE: &str = "save";
|
||||
const CMD_SEND: &str = "send";
|
||||
const CMD_WRITE: &str = "write";
|
||||
|
||||
pub(crate) const CMD_TPL: &str = "template";
|
||||
pub const CMD_TPL: &str = "template";
|
||||
|
||||
type Tpl<'a> = &'a str;
|
||||
pub type RawTpl = String;
|
||||
pub type Headers<'a> = Option<Vec<&'a str>>;
|
||||
pub type Body<'a> = Option<&'a str>;
|
||||
|
||||
/// Represents the template commands.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Cmd<'a> {
|
||||
Forward(email::args::Id<'a>, TplOverride<'a>),
|
||||
New(TplOverride<'a>),
|
||||
Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>),
|
||||
Save(email::args::Attachments<'a>, Tpl<'a>),
|
||||
Send(email::args::Attachments<'a>, Tpl<'a>),
|
||||
Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
|
||||
Write(Headers<'a>, Body<'a>),
|
||||
Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
|
||||
Save(RawTpl),
|
||||
Send(RawTpl),
|
||||
}
|
||||
|
||||
/// Represents the template command matcher.
|
||||
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
||||
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
|
||||
debug!("forward subcommand matched");
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::Forward(id, tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_NEW) {
|
||||
debug!("new subcommand matched");
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::New(tpl))
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Forward(id, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
|
||||
debug!("reply subcommand matched");
|
||||
let id = email::args::parse_id_arg(m);
|
||||
let all = email::args::parse_reply_all_flag(m);
|
||||
let tpl = parse_override_arg(m);
|
||||
Some(Cmd::Reply(id, all, tpl))
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Reply(id, all, headers, body))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
|
||||
debug!("save subcommand matched");
|
||||
let attachments = email::args::parse_attachments_arg(m);
|
||||
let tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Save(attachments, tpl))
|
||||
let raw_tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Save(raw_tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
|
||||
debug!("send subcommand matched");
|
||||
let attachments = email::args::parse_attachments_arg(m);
|
||||
let tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Send(attachments, tpl))
|
||||
let raw_tpl = parse_raw_arg(m);
|
||||
Some(Cmd::Send(raw_tpl))
|
||||
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
|
||||
let headers = parse_headers_arg(m);
|
||||
let body = parse_body_arg(m);
|
||||
Some(Cmd::Write(headers, body))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
@ -74,112 +64,76 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
}
|
||||
|
||||
/// Represents the template subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![SubCommand::with_name(CMD_TPL)
|
||||
.aliases(&["tpl"])
|
||||
pub fn subcmds<'a>() -> Vec<Command> {
|
||||
vec![Command::new(CMD_TPL)
|
||||
.alias("tpl")
|
||||
.about("Handles email templates")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.subcommand_required(true)
|
||||
.arg_required_else_help(true)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_NEW)
|
||||
.aliases(&["n"])
|
||||
.about("Generates a template for a new email")
|
||||
Command::new(CMD_FORWARD)
|
||||
.alias("fwd")
|
||||
.about("Generates a template for forwarding an email")
|
||||
.arg(email::args::id_arg())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_REPLY)
|
||||
.aliases(&["rep", "re", "r"])
|
||||
Command::new(CMD_REPLY)
|
||||
.about("Generates a template for replying to an email")
|
||||
.arg(email::args::id_arg())
|
||||
.arg(email::args::reply_all_flag())
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_FORWARD)
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a template for forwarding an email")
|
||||
.arg(email::args::id_arg())
|
||||
Command::new(CMD_SAVE)
|
||||
.about("Compiles the template into a valid email then saves it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_SEND)
|
||||
.about("Compiles the template into a valid email then sends it")
|
||||
.arg(Arg::new(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
Command::new(CMD_WRITE)
|
||||
.aliases(["new", "n"])
|
||||
.about("Generates a template for writing a new email")
|
||||
.args(&args()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SAVE)
|
||||
.about("Saves an email based on the given template")
|
||||
.arg(&email::args::attachments_arg())
|
||||
.arg(Arg::with_name(ARG_TPL).raw(true)),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name(CMD_SEND)
|
||||
.about("Sends an email based on the given template")
|
||||
.arg(&email::args::attachments_arg())
|
||||
.arg(Arg::with_name(ARG_TPL).raw(true)),
|
||||
)]
|
||||
}
|
||||
|
||||
/// Represents the template arguments.
|
||||
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
pub fn args() -> Vec<Arg> {
|
||||
vec![
|
||||
Arg::with_name(ARG_SUBJECT)
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name(ARG_FROM)
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_TO)
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_CC)
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_BCC)
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_HEADERS)
|
||||
Arg::new(ARG_HEADERS)
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.short('H')
|
||||
.long("header")
|
||||
.value_name("KEY:VAL")
|
||||
.multiple(true),
|
||||
Arg::with_name(ARG_BODY)
|
||||
.action(ArgAction::Append),
|
||||
Arg::new(ARG_BODY)
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.short('B')
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
Arg::with_name(ARG_SIGNATURE)
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
/// Represents the template override argument parser.
|
||||
pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride {
|
||||
TplOverride {
|
||||
subject: matches.value_of(ARG_SUBJECT),
|
||||
from: matches.values_of(ARG_FROM).map(Iterator::collect),
|
||||
to: matches.values_of(ARG_TO).map(Iterator::collect),
|
||||
cc: matches.values_of(ARG_CC).map(Iterator::collect),
|
||||
bcc: matches.values_of(ARG_BCC).map(Iterator::collect),
|
||||
headers: matches.values_of(ARG_HEADERS).map(Iterator::collect),
|
||||
body: matches.value_of(ARG_BODY),
|
||||
signature: matches.value_of(ARG_SIGNATURE),
|
||||
}
|
||||
/// Represents the template headers argument parser.
|
||||
pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> {
|
||||
m.get_many(ARG_HEADERS)
|
||||
.map(|h| h.map(String::as_str).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Represents the template body argument parser.
|
||||
pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> {
|
||||
matches.get_one::<String>(ARG_BODY).map(String::as_str)
|
||||
}
|
||||
|
||||
/// Represents the raw template argument parser.
|
||||
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
|
||||
matches.value_of(ARG_TPL).unwrap_or_default()
|
||||
pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl {
|
||||
matches
|
||||
.get_one::<String>(ARG_TPL)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
|
|
@ -1,103 +1,119 @@
|
|||
//! Module related to message template handling.
|
||||
//!
|
||||
//! This module gathers all message template commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{anyhow, Result};
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{AccountConfig, Backend, Email, Sender, TplOverride};
|
||||
use std::io::{self, BufRead};
|
||||
use himalaya_lib::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl};
|
||||
use std::io::{stdin, BufRead};
|
||||
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Generate a new message template.
|
||||
pub fn new<'a, P: Printer>(
|
||||
opts: TplOverride<'a>,
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
pub fn forward<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl = Email::default().to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
let tpl = backend
|
||||
.get_emails(folder, vec![id])?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_forward_tpl_builder(config)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
|
||||
printer.print(<Tpl as Into<String>>::into(tpl))
|
||||
}
|
||||
|
||||
/// Generate a reply message template.
|
||||
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
pub fn reply<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
id: &str,
|
||||
all: bool,
|
||||
opts: TplOverride<'_>,
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
.get_emails(folder, vec![id])?
|
||||
.first()
|
||||
.ok_or_else(|| anyhow!("cannot find email {}", id))?
|
||||
.to_reply_tpl_builder(config, all)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
|
||||
printer.print(<Tpl as Into<String>>::into(tpl))
|
||||
}
|
||||
|
||||
/// Generate a forward message template.
|
||||
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
opts: TplOverride<'_>,
|
||||
mbox: &str,
|
||||
pub fn save<P: Printer, B: Backend + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
folder: &str,
|
||||
tpl: String,
|
||||
) -> Result<()> {
|
||||
let tpl = backend
|
||||
.email_get(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.to_tpl(opts, config)?;
|
||||
printer.print_struct(tpl)
|
||||
}
|
||||
|
||||
/// Saves a message based on a template.
|
||||
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
config: &AccountConfig,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let raw_email = email.into_sendable(config)?.formatted();
|
||||
backend.email_add(mbox, &raw_email, "seen")?;
|
||||
printer.print_struct("Template successfully saved")
|
||||
})
|
||||
.compile(
|
||||
CompilerBuilder::default()
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()),
|
||||
)?;
|
||||
|
||||
backend.add_email(folder, &email, &Flags::default())?;
|
||||
printer.print("Template successfully saved!")
|
||||
}
|
||||
|
||||
/// Sends a message based on a template.
|
||||
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
mbox: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
tpl: &str,
|
||||
pub fn send<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
folder: &str,
|
||||
tpl: String,
|
||||
) -> Result<()> {
|
||||
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
|
||||
tpl.replace("\r", "")
|
||||
} else {
|
||||
io::stdin()
|
||||
stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
};
|
||||
let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
|
||||
let sent_msg = sender.send(&email)?;
|
||||
backend.email_add(mbox, &sent_msg, "seen")?;
|
||||
printer.print_struct("Template successfully sent")
|
||||
})
|
||||
.compile(
|
||||
CompilerBuilder::default()
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()),
|
||||
)?;
|
||||
sender.send(&email)?;
|
||||
backend.add_email(folder, &email, &Flags::default())?;
|
||||
printer.print("Template successfully sent!")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write<'a, P: Printer>(
|
||||
config: &'a AccountConfig,
|
||||
printer: &'a mut P,
|
||||
headers: Option<Vec<&str>>,
|
||||
body: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let tpl = Email::new_tpl_builder(config)?
|
||||
.set_some_raw_headers(headers)
|
||||
.some_text_plain_part(body)
|
||||
.build();
|
||||
|
||||
printer.print(<Tpl as Into<String>>::into(tpl))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
pub mod cache;
|
||||
pub mod compl;
|
||||
pub mod config;
|
||||
pub mod domain;
|
||||
pub mod man;
|
||||
pub mod output;
|
||||
pub mod printer;
|
||||
pub mod ui;
|
||||
|
|
379
src/main.rs
379
src/main.rs
|
@ -1,12 +1,12 @@
|
|||
use anyhow::{Context, Result};
|
||||
use std::env;
|
||||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use std::{borrow::Cow, env};
|
||||
use url::Url;
|
||||
|
||||
use himalaya::{
|
||||
account, compl,
|
||||
account, cache, compl,
|
||||
config::{self, DeserializedConfig},
|
||||
email, flag, folder,
|
||||
output::{self, OutputFmt},
|
||||
email, flag, folder, man, output,
|
||||
printer::StdoutPrinter,
|
||||
tpl,
|
||||
};
|
||||
|
@ -15,19 +15,22 @@ use himalaya_lib::{BackendBuilder, BackendConfig, ImapBackend, SenderBuilder};
|
|||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya::imap;
|
||||
|
||||
fn create_app<'a>() -> clap::App<'a, 'a> {
|
||||
let app = clap::App::new(env!("CARGO_PKG_NAME"))
|
||||
fn create_app() -> Command {
|
||||
let app = Command::new(env!("CARGO_PKG_NAME"))
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about(env!("CARGO_PKG_DESCRIPTION"))
|
||||
.author(env!("CARGO_PKG_AUTHORS"))
|
||||
.global_setting(clap::AppSettings::GlobalVersion)
|
||||
.arg(&config::args::arg())
|
||||
.arg(&account::args::arg())
|
||||
.args(&output::args::args())
|
||||
.propagate_version(true)
|
||||
.infer_subcommands(true)
|
||||
.arg(config::args::arg())
|
||||
.arg(account::args::arg())
|
||||
.arg(cache::args::arg())
|
||||
.args(output::args::args())
|
||||
.arg(folder::args::source_arg())
|
||||
.subcommands(compl::args::subcmds())
|
||||
.subcommands(account::args::subcmds())
|
||||
.subcommands(folder::args::subcmds())
|
||||
.subcommand(compl::args::subcmd())
|
||||
.subcommand(man::args::subcmd())
|
||||
.subcommand(account::args::subcmd())
|
||||
.subcommand(folder::args::subcmd())
|
||||
.subcommands(email::args::subcmds());
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
|
@ -41,73 +44,99 @@ fn main() -> Result<()> {
|
|||
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
|
||||
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
|
||||
|
||||
// Check mailto command BEFORE app initialization.
|
||||
// checks mailto command before app initialization
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
let config = DeserializedConfig::from_opt_path(None)?;
|
||||
let (account_config, backend_config) = config.to_configs(None)?;
|
||||
let mut backend = BackendBuilder::build(&account_config, &backend_config)?;
|
||||
let mut backend = BackendBuilder::new().build(&account_config, &backend_config)?;
|
||||
let mut sender = SenderBuilder::build(&account_config)?;
|
||||
let mut printer = StdoutPrinter::from_fmt(OutputFmt::Plain);
|
||||
let mut printer = StdoutPrinter::default();
|
||||
|
||||
return email::handlers::mailto(
|
||||
&url,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
&url,
|
||||
);
|
||||
}
|
||||
|
||||
let app = create_app();
|
||||
let m = app.get_matches();
|
||||
|
||||
// Check completion command BEFORE entities and services initialization.
|
||||
// Related issue: https://github.com/soywod/himalaya/issues/115.
|
||||
// checks completion command before configs
|
||||
// https://github.com/soywod/himalaya/issues/115
|
||||
match compl::args::matches(&m)? {
|
||||
Some(compl::args::Command::Generate(shell)) => {
|
||||
Some(compl::args::Cmd::Generate(shell)) => {
|
||||
return compl::handlers::generate(create_app(), shell);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Init entities and services.
|
||||
// checks completion command before configs
|
||||
// https://github.com/soywod/himalaya/issues/115
|
||||
match man::args::matches(&m)? {
|
||||
Some(man::args::Cmd::GenerateAll(dir)) => {
|
||||
return man::handlers::generate(dir, create_app());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// inits config
|
||||
let config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m))?;
|
||||
let (account_config, backend_config) = config.to_configs(account::args::parse_arg(&m))?;
|
||||
let folder = account_config.folder_alias(folder::args::parse_source_arg(&m))?;
|
||||
|
||||
// Check IMAP commands.
|
||||
// checks IMAP commands
|
||||
#[cfg(feature = "imap-backend")]
|
||||
if let BackendConfig::Imap(imap_config) = backend_config {
|
||||
// FIXME: find a way to downcast `backend` instead.
|
||||
let mut imap = ImapBackend::new(&account_config, imap_config);
|
||||
if let BackendConfig::Imap(imap_config) = &backend_config {
|
||||
// FIXME: find a way to downcast `backend` instead of
|
||||
// recreating an instance.
|
||||
match imap::args::matches(&m)? {
|
||||
Some(imap::args::Command::Notify(keepalive)) => {
|
||||
return imap::handlers::notify(keepalive, &folder, &mut imap);
|
||||
Some(imap::args::Cmd::Notify(keepalive)) => {
|
||||
let imap =
|
||||
ImapBackend::new(Cow::Borrowed(&account_config), Cow::Borrowed(&imap_config))?;
|
||||
return imap::handlers::notify(&imap, &folder, keepalive);
|
||||
}
|
||||
Some(imap::args::Command::Watch(keepalive)) => {
|
||||
return imap::handlers::watch(keepalive, &folder, &mut imap);
|
||||
Some(imap::args::Cmd::Watch(keepalive)) => {
|
||||
let imap =
|
||||
ImapBackend::new(Cow::Borrowed(&account_config), Cow::Borrowed(&imap_config))?;
|
||||
return imap::handlers::watch(&imap, &folder, keepalive);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let mut backend = BackendBuilder::build(&account_config, &backend_config)?;
|
||||
// inits services
|
||||
let mut sender = SenderBuilder::build(&account_config)?;
|
||||
let mut printer = StdoutPrinter::from_opt_str(m.value_of("output"))?;
|
||||
let mut printer = StdoutPrinter::try_from(&m)?;
|
||||
let disable_cache = cache::args::parse_disable_cache_flag(&m);
|
||||
|
||||
// Check account commands.
|
||||
// checks account commands
|
||||
match account::args::matches(&m)? {
|
||||
Some(account::args::Cmd::List(max_width)) => {
|
||||
return account::handlers::list(max_width, &account_config, &config, &mut printer);
|
||||
}
|
||||
Some(account::args::Cmd::Sync(dry_run)) => {
|
||||
let backend = BackendBuilder::new()
|
||||
.sessions_pool_size(16)
|
||||
.disable_cache(true)
|
||||
.build(&account_config, &backend_config)?;
|
||||
account::handlers::sync(&account_config, &mut printer, backend.as_ref(), dry_run)?;
|
||||
backend.close()?;
|
||||
return Ok(());
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
// Check folder commands.
|
||||
// checks folder commands
|
||||
match folder::args::matches(&m)? {
|
||||
Some(folder::args::Cmd::List(max_width)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return folder::handlers::list(
|
||||
max_width,
|
||||
&account_config,
|
||||
|
@ -118,202 +147,270 @@ fn main() -> Result<()> {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
// Check message commands.
|
||||
// checks email commands
|
||||
match email::args::matches(&m)? {
|
||||
Some(email::args::Cmd::Attachments(seq)) => {
|
||||
Some(email::args::Cmd::Attachments(ids)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::attachments(
|
||||
seq,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Copy(seq, mbox_dst)) => {
|
||||
return email::handlers::copy(seq, &folder, mbox_dst, &mut printer, backend.as_mut());
|
||||
}
|
||||
Some(email::args::Cmd::Delete(seq)) => {
|
||||
return email::handlers::delete(seq, &folder, &mut printer, backend.as_mut());
|
||||
}
|
||||
Some(email::args::Cmd::Forward(seq, attachment_paths, encrypt)) => {
|
||||
return email::handlers::forward(
|
||||
seq,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
Some(email::args::Cmd::Copy(ids, to_folder)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::copy(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
to_folder,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Delete(ids)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::delete(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Forward(id, headers, body)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::forward(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::List(max_width, page_size, page)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::list(
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Move(seq, mbox_dst)) => {
|
||||
return email::handlers::move_(seq, &folder, mbox_dst, &mut printer, backend.as_mut());
|
||||
Some(email::args::Cmd::Move(ids, to_folder)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::move_(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
to_folder,
|
||||
ids,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Read(seq, text_mime, sanitize, raw, headers)) => {
|
||||
Some(email::args::Cmd::Read(ids, text_mime, sanitize, raw, headers)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::read(
|
||||
seq,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
ids,
|
||||
text_mime,
|
||||
sanitize,
|
||||
raw,
|
||||
headers,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
|
||||
Some(email::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::reply(
|
||||
seq,
|
||||
all,
|
||||
attachment_paths,
|
||||
encrypt,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
&folder,
|
||||
id,
|
||||
all,
|
||||
headers,
|
||||
body,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Save(raw_msg)) => {
|
||||
return email::handlers::save(&folder, raw_msg, &mut printer, backend.as_mut());
|
||||
Some(email::args::Cmd::Save(raw_email)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::save(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
raw_email,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Search(query, max_width, page_size, page)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::search(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::sort(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
criteria,
|
||||
query,
|
||||
max_width,
|
||||
page_size,
|
||||
page,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Send(raw_msg)) => {
|
||||
Some(email::args::Cmd::Send(raw_email)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::send(
|
||||
raw_msg,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Write(tpl, atts, encrypt)) => {
|
||||
return email::handlers::write(
|
||||
tpl,
|
||||
atts,
|
||||
encrypt,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
raw_email,
|
||||
);
|
||||
}
|
||||
Some(email::args::Cmd::Flag(m)) => match m {
|
||||
Some(flag::args::Cmd::Set(seq_range, ref flags)) => {
|
||||
return flag::handlers::set(
|
||||
seq_range,
|
||||
flags,
|
||||
&folder,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
Some(flag::args::Cmd::Set(ids, ref flags)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return flag::handlers::set(&mut printer, backend.as_mut(), &folder, ids, flags);
|
||||
}
|
||||
Some(flag::args::Cmd::Add(seq_range, ref flags)) => {
|
||||
return flag::handlers::add(
|
||||
seq_range,
|
||||
flags,
|
||||
&folder,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
Some(flag::args::Cmd::Add(ids, ref flags)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return flag::handlers::add(&mut printer, backend.as_mut(), &folder, ids, flags);
|
||||
}
|
||||
Some(flag::args::Cmd::Del(seq_range, ref flags)) => {
|
||||
return flag::handlers::remove(
|
||||
seq_range,
|
||||
flags,
|
||||
&folder,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
Some(flag::args::Cmd::Remove(ids, ref flags)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return flag::handlers::remove(&mut printer, backend.as_mut(), &folder, ids, flags);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Some(email::args::Cmd::Tpl(m)) => match m {
|
||||
Some(tpl::args::Cmd::New(tpl)) => {
|
||||
return tpl::handlers::new(tpl, &account_config, &mut printer);
|
||||
}
|
||||
Some(tpl::args::Cmd::Reply(seq, all, tpl)) => {
|
||||
return tpl::handlers::reply(
|
||||
seq,
|
||||
all,
|
||||
tpl,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
);
|
||||
}
|
||||
Some(tpl::args::Cmd::Forward(seq, tpl)) => {
|
||||
Some(tpl::args::Cmd::Forward(id, headers, body)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return tpl::handlers::forward(
|
||||
seq,
|
||||
tpl,
|
||||
&folder,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
id,
|
||||
headers,
|
||||
body,
|
||||
);
|
||||
}
|
||||
Some(tpl::args::Cmd::Save(atts, tpl)) => {
|
||||
Some(tpl::args::Cmd::Write(headers, body)) => {
|
||||
return tpl::handlers::write(&account_config, &mut printer, headers, body);
|
||||
}
|
||||
Some(tpl::args::Cmd::Reply(id, all, headers, body)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return tpl::handlers::reply(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
id,
|
||||
all,
|
||||
headers,
|
||||
body,
|
||||
);
|
||||
}
|
||||
Some(tpl::args::Cmd::Save(tpl)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return tpl::handlers::save(
|
||||
&folder,
|
||||
&account_config,
|
||||
atts,
|
||||
tpl,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
&folder,
|
||||
tpl,
|
||||
);
|
||||
}
|
||||
Some(tpl::args::Cmd::Send(atts, tpl)) => {
|
||||
Some(tpl::args::Cmd::Send(tpl)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return tpl::handlers::send(
|
||||
&folder,
|
||||
atts,
|
||||
tpl,
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
&folder,
|
||||
tpl,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
Some(email::args::Cmd::Write(headers, body)) => {
|
||||
let mut backend = BackendBuilder::new()
|
||||
.disable_cache(disable_cache)
|
||||
.build(&account_config, &backend_config)?;
|
||||
return email::handlers::write(
|
||||
&account_config,
|
||||
&mut printer,
|
||||
backend.as_mut(),
|
||||
sender.as_mut(),
|
||||
headers,
|
||||
body,
|
||||
);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
backend.as_mut().disconnect().context("cannot disconnect")
|
||||
Ok(())
|
||||
}
|
||||
|
|
40
src/man/args.rs
Normal file
40
src/man/args.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
//! Module related to man CLI.
|
||||
//!
|
||||
//! This module provides subcommands and a command matcher related to
|
||||
//! man.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use log::debug;
|
||||
|
||||
const ARG_DIR: &str = "dir";
|
||||
const CMD_MAN: &str = "man";
|
||||
|
||||
/// Man commands.
|
||||
pub enum Cmd<'a> {
|
||||
/// Generates all man pages to the specified directory.
|
||||
GenerateAll(&'a str),
|
||||
}
|
||||
|
||||
/// Man command matcher.
|
||||
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
|
||||
if let Some(m) = m.subcommand_matches(CMD_MAN) {
|
||||
let dir = m.get_one::<String>(ARG_DIR).map(String::as_str).unwrap();
|
||||
debug!("directory: {}", dir);
|
||||
return Ok(Some(Cmd::GenerateAll(dir)));
|
||||
};
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Man subcommands.
|
||||
pub fn subcmd() -> Command {
|
||||
Command::new(CMD_MAN)
|
||||
.about("Generates all man pages to the specified directory.")
|
||||
.arg(
|
||||
Arg::new(ARG_DIR)
|
||||
.help("Directory where to generate man files")
|
||||
.long_help("Represents the directory where all man files of all commands and subcommands should be generated in.")
|
||||
.required(true),
|
||||
)
|
||||
}
|
29
src/man/handlers.rs
Normal file
29
src/man/handlers.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
//! Module related to man handling.
|
||||
//!
|
||||
//! This module gathers all man commands.
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Command;
|
||||
use clap_mangen::Man;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
/// Generates all man pages of all subcommands in the given directory.
|
||||
pub fn generate(dir: &str, cmd: Command) -> Result<()> {
|
||||
let mut buffer = Vec::new();
|
||||
let cmd_name = cmd.get_name().to_string();
|
||||
let subcmds = cmd.get_subcommands().cloned().collect::<Vec<_>>();
|
||||
Man::new(cmd).render(&mut buffer)?;
|
||||
fs::write(PathBuf::from(dir).join(format!("{}.1", cmd_name)), buffer)?;
|
||||
|
||||
for subcmd in subcmds {
|
||||
let mut buffer = Vec::new();
|
||||
let subcmd_name = subcmd.get_name().to_string();
|
||||
Man::new(subcmd).render(&mut buffer)?;
|
||||
fs::write(
|
||||
PathBuf::from(dir).join(format!("{}-{}.1", cmd_name, subcmd_name)),
|
||||
buffer,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
2
src/man/mod.rs
Normal file
2
src/man/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -4,23 +4,43 @@
|
|||
|
||||
use clap::Arg;
|
||||
|
||||
pub(crate) const ARG_COLOR: &str = "color";
|
||||
pub(crate) const ARG_OUTPUT: &str = "output";
|
||||
|
||||
/// Output arguments.
|
||||
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
|
||||
pub fn args() -> Vec<Arg> {
|
||||
vec![
|
||||
Arg::with_name("output")
|
||||
.long("output")
|
||||
.short("o")
|
||||
Arg::new(ARG_OUTPUT)
|
||||
.help("Defines the output format")
|
||||
.long("output")
|
||||
.short('o')
|
||||
.value_name("FMT")
|
||||
.possible_values(&["plain", "json"])
|
||||
.value_parser(["plain", "json"])
|
||||
.default_value("plain"),
|
||||
Arg::with_name("log-level")
|
||||
.long("log-level")
|
||||
.alias("log")
|
||||
.short("l")
|
||||
.help("Defines the logs level")
|
||||
.value_name("LEVEL")
|
||||
.possible_values(&["error", "warn", "info", "debug", "trace"])
|
||||
.default_value("info"),
|
||||
Arg::new(ARG_COLOR)
|
||||
.help("Controls when to use colors.")
|
||||
.long_help(
|
||||
"
|
||||
This flag controls when to use colors. The default setting is 'auto', which
|
||||
means himalaya will try to guess when to use colors. For example, if himalaya is
|
||||
printing to a terminal, then it will use colors, but if it is redirected to a
|
||||
file or a pipe, then it will suppress color output. himalaya will suppress color
|
||||
output in some other circumstances as well. For example, if the TERM
|
||||
environment variable is not set or set to 'dumb', then himalaya will not use
|
||||
colors.
|
||||
|
||||
The possible values for this flag are:
|
||||
|
||||
never Colors will never be used.
|
||||
auto The default. himalaya tries to be smart.
|
||||
always Colors will always be used regardless of where output is sent.
|
||||
ansi Like 'always', but emits ANSI escapes (even in a Windows console).
|
||||
",
|
||||
)
|
||||
.long("color")
|
||||
.short('C')
|
||||
.value_parser(["never", "auto", "always", "ansi"])
|
||||
.default_value("auto")
|
||||
.value_name("WHEN"),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,31 +1,30 @@
|
|||
use anyhow::{anyhow, Error, Result};
|
||||
use std::{convert::TryFrom, fmt};
|
||||
use atty::Stream;
|
||||
use serde::Serialize;
|
||||
use std::{fmt, str::FromStr};
|
||||
use termcolor::ColorChoice;
|
||||
|
||||
/// Represents the available output formats.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum OutputFmt {
|
||||
Plain,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl From<&str> for OutputFmt {
|
||||
fn from(fmt: &str) -> Self {
|
||||
match fmt {
|
||||
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
|
||||
_ => Self::Plain,
|
||||
}
|
||||
impl Default for OutputFmt {
|
||||
fn default() -> Self {
|
||||
Self::Plain
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Option<&str>> for OutputFmt {
|
||||
type Error = Error;
|
||||
impl FromStr for OutputFmt {
|
||||
type Err = Error;
|
||||
|
||||
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
|
||||
fn from_str(fmt: &str) -> Result<Self, Self::Err> {
|
||||
match fmt {
|
||||
Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
|
||||
Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
|
||||
None => Ok(Self::Plain),
|
||||
Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)),
|
||||
fmt if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
|
||||
fmt if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
|
||||
unknown => Err(anyhow!("cannot parse output format {}", unknown)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -39,3 +38,76 @@ impl fmt::Display for OutputFmt {
|
|||
write!(f, "{}", fmt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines a struct-wrapper to provide a JSON output.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub struct OutputJson<T: Serialize> {
|
||||
response: T,
|
||||
}
|
||||
|
||||
impl<T: Serialize> OutputJson<T> {
|
||||
pub fn new(response: T) -> Self {
|
||||
Self { response }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represent the available color configs.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ColorFmt {
|
||||
Never,
|
||||
Always,
|
||||
Ansi,
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl Default for ColorFmt {
|
||||
fn default() -> Self {
|
||||
Self::Auto
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ColorFmt {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(fmt: &str) -> Result<Self, Self::Err> {
|
||||
match fmt {
|
||||
fmt if fmt.eq_ignore_ascii_case("never") => Ok(Self::Never),
|
||||
fmt if fmt.eq_ignore_ascii_case("always") => Ok(Self::Always),
|
||||
fmt if fmt.eq_ignore_ascii_case("ansi") => Ok(Self::Ansi),
|
||||
fmt if fmt.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
|
||||
unknown => Err(anyhow!("cannot parse color format {}", unknown)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ColorFmt> for ColorChoice {
|
||||
fn from(fmt: ColorFmt) -> Self {
|
||||
match fmt {
|
||||
ColorFmt::Never => Self::Never,
|
||||
ColorFmt::Always => Self::Always,
|
||||
ColorFmt::Ansi => Self::AlwaysAnsi,
|
||||
ColorFmt::Auto => {
|
||||
if atty::is(Stream::Stdout) {
|
||||
// Otherwise let's `termcolor` decide by
|
||||
// inspecting the environment. From the [doc]:
|
||||
//
|
||||
// * If `NO_COLOR` is set to any value, then
|
||||
// colors will be suppressed.
|
||||
//
|
||||
// * If `TERM` is set to dumb, then colors will be
|
||||
// suppressed.
|
||||
//
|
||||
// * In non-Windows environments, if `TERM` is not
|
||||
// set, then colors will be suppressed.
|
||||
//
|
||||
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
|
||||
Self::Auto
|
||||
} else {
|
||||
// Colors should be deactivated if the terminal is
|
||||
// not a tty.
|
||||
Self::Never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::Tpl;
|
||||
|
||||
use crate::printer::WriteColor;
|
||||
|
||||
|
@ -19,3 +20,10 @@ impl Print for String {
|
|||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Print for Tpl {
|
||||
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
|
||||
self.as_str().print(writer)?;
|
||||
Ok(writer.reset()?)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use anyhow::{Context, Error, Result};
|
||||
use clap::ArgMatches;
|
||||
use std::fmt::{self, Debug};
|
||||
use termcolor::{ColorChoice, StandardStream};
|
||||
use termcolor::StandardStream;
|
||||
|
||||
use crate::{
|
||||
output::OutputFmt,
|
||||
output::{args, ColorFmt, OutputFmt},
|
||||
printer::{Print, PrintTable, PrintTableOpts, WriteColor},
|
||||
};
|
||||
|
||||
pub trait Printer {
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
|
||||
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()>;
|
||||
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
|
@ -24,41 +24,30 @@ pub struct StdoutPrinter {
|
|||
pub fmt: OutputFmt,
|
||||
}
|
||||
|
||||
impl StdoutPrinter {
|
||||
pub fn from_fmt(fmt: OutputFmt) -> Self {
|
||||
let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
|
||||
// Colors should be deactivated if the terminal is not a tty.
|
||||
ColorChoice::Never
|
||||
} else {
|
||||
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
|
||||
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
|
||||
// - If `TERM` is set to dumb, then colors will be suppressed.
|
||||
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
|
||||
//
|
||||
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
|
||||
ColorChoice::Auto
|
||||
});
|
||||
let writer = Box::new(writer);
|
||||
Self { writer, fmt }
|
||||
impl Default for StdoutPrinter {
|
||||
fn default() -> Self {
|
||||
let fmt = OutputFmt::default();
|
||||
let writer = Box::new(StandardStream::stdout(ColorFmt::default().into()));
|
||||
Self { fmt, writer }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_opt_str(s: Option<&str>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
fmt: OutputFmt::try_from(s)?,
|
||||
..Self::from_fmt(OutputFmt::Plain)
|
||||
})
|
||||
impl StdoutPrinter {
|
||||
pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self {
|
||||
let writer = Box::new(StandardStream::stdout(color.into()));
|
||||
Self { fmt, writer }
|
||||
}
|
||||
}
|
||||
|
||||
impl Printer for StdoutPrinter {
|
||||
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()> {
|
||||
fn print_log<T: Debug + Print>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
|
||||
fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => data.print(self.writer.as_mut()),
|
||||
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data)
|
||||
|
@ -86,3 +75,29 @@ impl Printer for StdoutPrinter {
|
|||
self.fmt == OutputFmt::Json
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutputFmt> for StdoutPrinter {
|
||||
fn from(fmt: OutputFmt) -> Self {
|
||||
Self::new(fmt, ColorFmt::Auto)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&ArgMatches> for StdoutPrinter {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(m: &ArgMatches) -> Result<Self, Self::Error> {
|
||||
let fmt: OutputFmt = m
|
||||
.get_one::<String>(args::ARG_OUTPUT)
|
||||
.map(String::as_str)
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
|
||||
let color: ColorFmt = m
|
||||
.get_one::<String>(args::ARG_COLOR)
|
||||
.map(String::as_str)
|
||||
.unwrap()
|
||||
.parse()?;
|
||||
|
||||
Ok(Self::new(fmt, color))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::{
|
||||
email::{local_draft_path, remove_local_draft, Email, TplOverride},
|
||||
AccountConfig, Backend, Sender,
|
||||
email::{local_draft_path, remove_local_draft},
|
||||
AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl,
|
||||
};
|
||||
use log::{debug, info};
|
||||
use log::debug;
|
||||
use std::{env, fs, process::Command};
|
||||
|
||||
use crate::{
|
||||
|
@ -11,7 +11,7 @@ use crate::{
|
|||
ui::choice::{self, PostEditChoice, PreEditChoice},
|
||||
};
|
||||
|
||||
pub fn open_with_tpl(tpl: String) -> Result<String> {
|
||||
pub fn open_with_tpl(tpl: Tpl) -> Result<Tpl> {
|
||||
let path = local_draft_path();
|
||||
|
||||
debug!("create draft");
|
||||
|
@ -27,48 +27,34 @@ pub fn open_with_tpl(tpl: String) -> Result<String> {
|
|||
let content =
|
||||
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
|
||||
|
||||
Ok(content)
|
||||
Ok(Tpl::from(content))
|
||||
}
|
||||
|
||||
pub fn open_with_draft() -> Result<String> {
|
||||
pub fn open_with_local_draft() -> Result<Tpl> {
|
||||
let path = local_draft_path();
|
||||
let tpl =
|
||||
let content =
|
||||
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
|
||||
open_with_tpl(tpl)
|
||||
open_with_tpl(Tpl::from(content))
|
||||
}
|
||||
|
||||
fn _edit_email_with_editor(
|
||||
email: &Email,
|
||||
tpl: TplOverride,
|
||||
config: &AccountConfig,
|
||||
) -> Result<Email> {
|
||||
let tpl = email.to_tpl(tpl, config)?;
|
||||
let tpl = open_with_tpl(tpl)?;
|
||||
Email::from_tpl(&tpl).context("cannot parse email from template")
|
||||
}
|
||||
|
||||
pub fn edit_email_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
mut email: Email,
|
||||
tpl: TplOverride,
|
||||
pub fn edit_tpl_with_editor<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
mut tpl: Tpl,
|
||||
) -> Result<()> {
|
||||
info!("start editing with editor");
|
||||
|
||||
let draft = local_draft_path();
|
||||
if draft.exists() {
|
||||
loop {
|
||||
match choice::pre_edit() {
|
||||
Ok(choice) => match choice {
|
||||
PreEditChoice::Edit => {
|
||||
let tpl = open_with_draft()?;
|
||||
email.merge_with(Email::from_tpl(&tpl)?);
|
||||
tpl = open_with_local_draft()?;
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Discard => {
|
||||
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?);
|
||||
tpl = open_with_tpl(tpl)?;
|
||||
break;
|
||||
}
|
||||
PreEditChoice::Quit => return Ok(()),
|
||||
|
@ -80,35 +66,44 @@ pub fn edit_email_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender
|
|||
}
|
||||
}
|
||||
} else {
|
||||
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?);
|
||||
tpl = open_with_tpl(tpl)?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match choice::post_edit() {
|
||||
Ok(PostEditChoice::Send) => {
|
||||
printer.print_str("Sending email…")?;
|
||||
let sent_email: Vec<u8> = sender.send(&email)?;
|
||||
let sent_folder = config.folder_alias("sent")?;
|
||||
printer.print_str(format!("Adding email to the {:?} folder…", sent_folder))?;
|
||||
backend.email_add(&sent_folder, &sent_email, "seen")?;
|
||||
printer.print_log("Sending email…")?;
|
||||
let email = tpl.compile(
|
||||
CompilerBuilder::default()
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()),
|
||||
)?;
|
||||
sender.send(&email)?;
|
||||
let sent_folder = config.sent_folder_alias()?;
|
||||
printer.print_log(format!("Adding email to the {} folder…", sent_folder))?;
|
||||
backend.add_email(&sent_folder, &email, &Flags::default())?;
|
||||
remove_local_draft()?;
|
||||
printer.print_struct("Done!")?;
|
||||
printer.print("Done!")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Edit) => {
|
||||
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?);
|
||||
tpl = open_with_tpl(tpl)?;
|
||||
continue;
|
||||
}
|
||||
Ok(PostEditChoice::LocalDraft) => {
|
||||
printer.print_struct("Email successfully saved locally")?;
|
||||
printer.print("Email successfully saved locally")?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::RemoteDraft) => {
|
||||
let tpl = email.to_tpl(TplOverride::default(), config)?;
|
||||
let draft_folder = config.folder_alias("drafts")?;
|
||||
backend.email_add(&draft_folder, tpl.as_bytes(), "seen draft")?;
|
||||
let email = tpl.compile(
|
||||
CompilerBuilder::default()
|
||||
.some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
|
||||
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.as_ref()),
|
||||
)?;
|
||||
backend.add_email(&draft_folder, &email, &Flags::from_iter([Flag::Draft]))?;
|
||||
remove_local_draft()?;
|
||||
printer.print_struct(format!("Email successfully saved to {}", draft_folder))?;
|
||||
printer.print(format!("Email successfully saved to {}", draft_folder))?;
|
||||
break;
|
||||
}
|
||||
Ok(PostEditChoice::Discard) => {
|
||||
|
|
|
@ -5,17 +5,17 @@ const ARG_MAX_TABLE_WIDTH: &str = "max-table-width";
|
|||
pub(crate) type MaxTableWidth = Option<usize>;
|
||||
|
||||
/// Represents the max table width argument.
|
||||
pub fn max_width<'a>() -> Arg<'a, 'a> {
|
||||
Arg::with_name(ARG_MAX_TABLE_WIDTH)
|
||||
pub fn max_width() -> Arg {
|
||||
Arg::new(ARG_MAX_TABLE_WIDTH)
|
||||
.help("Defines a maximum width for the table")
|
||||
.short("w")
|
||||
.long("max-width")
|
||||
.short('w')
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
/// Represents the max table width argument parser.
|
||||
pub fn parse_max_width<'a>(matches: &'a ArgMatches<'a>) -> Option<usize> {
|
||||
pub fn parse_max_width(matches: &ArgMatches) -> Option<usize> {
|
||||
matches
|
||||
.value_of(ARG_MAX_TABLE_WIDTH)
|
||||
.and_then(|width| width.parse::<usize>().ok())
|
||||
.get_one::<String>(ARG_MAX_TABLE_WIDTH)
|
||||
.and_then(|s| s.parse().ok())
|
||||
}
|
||||
|
|
|
@ -183,7 +183,7 @@ where
|
|||
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
|
||||
table.extend(
|
||||
items
|
||||
.iter()
|
||||
.into_iter()
|
||||
.map(|item| {
|
||||
let row = item.row();
|
||||
row.0.iter().enumerate().for_each(|(i, cell)| {
|
||||
|
|
Loading…
Reference in a new issue