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:
Clément DOUIN 2023-02-08 16:03:45 +01:00 committed by GitHub
parent bda37ca0ed
commit 694173b534
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
52 changed files with 3154 additions and 1664 deletions

View 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)

View file

@ -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

View file

@ -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
View 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

View file

@ -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:

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
View file

@ -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)

View file

@ -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
View 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
View file

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

View file

@ -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)])
}

View file

@ -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(())
}

View file

@ -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)
}

View file

@ -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))
}

View file

@ -1,5 +1,6 @@
pub mod args;
pub mod config;
pub mod prelude;
mod wizard;
pub use config::*;

View file

@ -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
View 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 },
))
}

View 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
View 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)
}

View 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 },
))
}

View 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
View 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))
}

View 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))
}
}
}

View file

@ -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)
}

View file

@ -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)]

View file

@ -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 {

View file

@ -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());

View file

@ -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(())
}

View file

@ -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())

View file

@ -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),
)
}

View file

@ -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!")
}

View file

@ -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)
);
}
}

View file

@ -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
}
}

View file

@ -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))
}

View file

@ -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")
}

View file

@ -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()
}

View file

@ -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))
}

View file

@ -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;

View file

@ -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
View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod args;
pub mod handlers;

View file

@ -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"),
]
}

View file

@ -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
}
}
}
}
}

View file

@ -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()?)
}
}

View file

@ -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))
}
}

View file

@ -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) => {

View file

@ -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())
}

View file

@ -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)| {