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 }} release_name: ${{ github.ref }}
draft: false draft: false
prerelease: false prerelease: false
deploy_github: deploy_linux_macos_windows_github:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: create_release needs: create_release
strategy: strategy:
@ -47,7 +47,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: check command: check
- name: Build release - name: Builds release
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: build command: build
@ -67,6 +67,26 @@ jobs:
asset_path: himalaya.tar.gz asset_path: himalaya.tar.gz
asset_name: himalaya-${{ matrix.os_name }}.tar.gz asset_name: himalaya-${{ matrix.os_name }}.tar.gz
asset_content_type: application/gzip 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: deploy_crates:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: create_release 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 3465:3465 \
-p 3993:3993 \ -p 3993:3993 \
-p 3995:3995 \ -p 3995:3995 \
greenmail/standalone:1.6.2 greenmail/standalone:1.6.11
- name: Install rust - name: Install rust
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:

View file

@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.6.1] - 2022-10-12
### Added ### Added
@ -436,7 +474,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Password from command [#22] * Password from command [#22]
* Set up README [#20] * 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.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.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 [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 [#335]: https://github.com/soywod/himalaya/issues/335
[#338]: https://github.com/soywod/himalaya/issues/338 [#338]: https://github.com/soywod/himalaya/issues/338
[#340]: https://github.com/soywod/himalaya/issues/340 [#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 [#344]: https://github.com/soywod/himalaya/issues/344
[#346]: https://github.com/soywod/himalaya/issues/346 [#346]: https://github.com/soywod/himalaya/issues/346
[#352]: https://github.com/soywod/himalaya/issues/352 [#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! 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. If you want to **discuss** about the project, feel free to join the
[Matrix](https://matrix.org/) workspace
#### Make changes locally [#pimalaya](https://matrix.to/#/#pimalaya:matrix.org) or contact me
directly [@soywod](https://matrix.to/#/@soywod:matrix.org).
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.

910
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
[package] [package]
name = "himalaya" name = "himalaya"
description = "Command-line interface for email management." description = "Command-line interface for email management."
version = "0.6.1" version = "0.7.0"
authors = ["soywod <clement.douin@posteo.net>"] authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
@ -22,25 +22,30 @@ notmuch-backend = ["himalaya-lib/notmuch-backend"]
default = ["imap-backend", "maildir-backend"] default = ["imap-backend", "maildir-backend"]
[dev-dependencies] [dev-dependencies]
tempfile = "3.3.0" tempfile = "3.3"
[dependencies] [dependencies]
anyhow = "1.0.44" anyhow = "1.0"
atty = "0.2.14" atty = "0.2"
chrono = "0.4.19" clap = "4.0"
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } clap_complete = "4.0"
env_logger = "0.8.3" clap_mangen = "0.2"
erased-serde = "0.3.18" console = "0.15.2"
himalaya-lib = "=0.4.0" dirs = "4.0.0"
lettre = { version = "=0.10.0-rc.7", features = ["serde"] } dialoguer = "0.10.2"
log = "0.4.14" email_address = "0.2.4"
mailparse = "0.13.6" env_logger = "0.8"
serde = { version = "1.0.118", features = ["derive"] } erased-serde = "0.3"
serde_json = "1.0.61" himalaya-lib = "0.5"
shellexpand = "2.1.0" 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" termcolor = "1.1"
terminal_size = "0.1.15" terminal_size = "0.1"
toml = "0.5.8" toml = "0.5"
unicode-width = "0.1.7" unicode-width = "0.1"
url = "2.2.2" url = "2.2"
uuid = { version = "0.8", features = ["v4"] } 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 Command-line interface for email management based on the
[himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib). [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib).
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png) ![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 *Warning: the project is under active development, do not use in
before the `v1.0.0`.* 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 ## Installation
[![Packaging <table align="center">
status](https://repology.org/badge/vertical-allrepos/himalaya.svg)](https://repology.org/project/himalaya/versions) <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 ```shell
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh # 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 *Note: see the
[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary) [wiki](https://github.com/soywod/himalaya/wiki/Installation) for other
for other installation methods.* installation methods.*
</td>
</tr>
</table>
## Configuration ## Configuration
@ -34,61 +75,96 @@ signature = "Regards,"
default = true default = true
email = "test@gmail.com" email = "test@gmail.com"
backend = "imap" # imap, maildir or notmuch backend = "imap"
imap-host = "imap.gmail.com" imap-host = "imap.gmail.com"
imap-port = 993 imap-port = 993
imap-login = "test@gmail.com" 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-host = "smtp.gmail.com"
smtp-port = 465 smtp-port = 465
smtp-login = "test@gmail.com" smtp-login = "test@gmail.com"
smtp-passwd-cmd = "security find-internet-password -gs gmail -w" 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 *Note: see the
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file) [wiki](https://github.com/soywod/himalaya/wiki/Configuration) for all
for all the options.* the options.*
## Features ## Contributing
- Folder listing If you find a **bug**, please send an email at
- Email listing and searching [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
- 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
- …
*See the If you have a **question**, please send an email at
[wiki](https://github.com/soywod/himalaya/wiki/Usage:email:list) for [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
all the features.*
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 ## Credits
- [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib) [![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor Special thanks to the
- [isync](https://isync.sourceforge.io/), an email synchronizer for [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 offline usage
- [NeoMutt](https://neomutt.org/), an email terminal user interface * [NeoMutt](https://neomutt.org/), an email terminal user interface
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other * [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
email terminal user interface 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 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 ## Sponsoring
[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors&style=flat-square)](https://github.com/sponsors/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) [![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) [![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) [![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) [![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-compat": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1650374568, "lastModified": 1673956053,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=", "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra", "owner": "edolstra",
"repo": "flake-compat", "repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8", "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -18,11 +18,11 @@
}, },
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1656928814, "lastModified": 1659877975,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -36,11 +36,11 @@
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs"
}, },
"locked": { "locked": {
"lastModified": 1662220400, "lastModified": 1671096816,
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=", "narHash": "sha256-ezQCsNgmpUHdZANDCILm3RvtO1xH8uujk/+EqNvzIOg=",
"owner": "nix-community", "owner": "nix-community",
"repo": "naersk", "repo": "naersk",
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3", "rev": "d998160d6a076cfe8f9741e56aeec7e267e3e114",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -51,11 +51,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1664356419, "lastModified": 1675698036,
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=", "narHash": "sha256-BgsQkQewdlQi8gapJN4phpxkI/FCE/2sORBaFcYbp/A=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1", "rev": "1046c7b92e908a1202c0f1ba3fc21d19e1cf1b62",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -79,11 +79,11 @@
}, },
"nixpkgs_3": { "nixpkgs_3": {
"locked": { "locked": {
"lastModified": 1659102345, "lastModified": 1665296151,
"narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=", "narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7", "rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -108,11 +108,11 @@
"nixpkgs": "nixpkgs_3" "nixpkgs": "nixpkgs_3"
}, },
"locked": { "locked": {
"lastModified": 1664334084, "lastModified": 1675823425,
"narHash": "sha256-cqP0TzDs3GDRprS6IgVQcWjQ0ynmjQFjYWvp+LE/s6I=", "narHash": "sha256-o/uLXQdq3OrRAv4BZVVY0VmhMmQBLWw6Y4o+p6ZiaR4=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "70eab96a255ae9b4b82b38ea5ac5c8e5b57e0abd", "rev": "02e1abbdcbc2d516193ff8a7add71f44cd976ba0",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -123,11 +123,11 @@
}, },
"utils": { "utils": {
"locked": { "locked": {
"lastModified": 1659877975, "lastModified": 1667395993,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
"type": "github" "type": "github"
}, },
"original": { "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. //! This module provides subcommands and a command matcher related to completion.
use anyhow::Result; use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, Shell, SubCommand}; use clap::{value_parser, Arg, ArgMatches, Command};
use log::{debug, info}; 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. /// Completion commands.
pub enum Command<'a> { pub enum Cmd {
/// Generate completion script for the given shell slice. /// Generate completion script for the given shell.
Generate(OptionShell<'a>), Generate(SomeShell),
} }
/// Completion command matcher. /// Completion command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> { pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
info!("entering completion command matcher"); if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
if let Some(m) = m.subcommand_matches("completion") {
info!("completion command matched");
let shell = m.value_of("shell");
debug!("shell: {:?}", shell); debug!("shell: {:?}", shell);
return Ok(Some(Command::Generate(shell))); return Ok(Some(Cmd::Generate(shell)));
}; };
Ok(None) Ok(None)
} }
/// Completion subcommands. /// Completion subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmd() -> Command {
vec![SubCommand::with_name("completion") Command::new(CMD_COMPLETION)
.aliases(&["completions", "compl", "compe", "comp"])
.about("Generates the completion script for the given shell") .about("Generates the completion script for the given shell")
.args(&[Arg::with_name("shell") .args(&[Arg::new(ARG_SHELL)
.possible_values(&Shell::variants()[..]) .value_parser(value_parser!(Shell))
.required(true)])] .required(true)])
} }

View file

@ -2,20 +2,14 @@
//! //!
//! This module gathers all completion commands. //! This module gathers all completion commands.
use anyhow::{anyhow, Context, Result}; use anyhow::Result;
use clap::{App, Shell}; use clap::Command;
use log::{debug, info}; use clap_complete::Shell;
use std::{io, str::FromStr}; use std::io::stdout;
/// Generates completion script from the given [`clap::App`] for the given shell slice. /// 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<()> { pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
info!("entering generate completion handler"); let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
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());
Ok(()) Ok(())
} }

View file

@ -6,15 +6,15 @@ const ARG_CONFIG: &str = "config";
/// Represents the config file path argument. This argument allows the /// Represents the config file path argument. This argument allows the
/// user to customize the config file path. /// user to customize the config file path.
pub fn arg<'a>() -> Arg<'a, 'a> { pub fn arg() -> Arg {
Arg::with_name(ARG_CONFIG) Arg::new(ARG_CONFIG)
.long("config") .long("config")
.short("c") .short('c')
.help("Forces a specific config file path") .help("Forces a specific config file path")
.value_name("PATH") .value_name("PATH")
} }
/// Represents the config file path argument parser. /// Represents the config file path argument parser.
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> { pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
matches.value_of(ARG_CONFIG) matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
} }

View file

@ -4,16 +4,20 @@
//! user configuration file. //! user configuration file.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use dirs::{config_dir, home_dir};
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat}; use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
use log::{debug, trace}; use log::{debug, trace};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, env, fs, path::PathBuf}; use std::{collections::HashMap, fs, path::PathBuf};
use toml; use toml;
use crate::{account::DeserializedAccountConfig, config::prelude::*}; use crate::{
account::DeserializedAccountConfig,
config::{prelude::*, wizard::wizard},
};
/// Represents the user config file. /// 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")] #[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig { pub struct DeserializedConfig {
#[serde(alias = "name")] #[serde(alias = "name")]
@ -27,11 +31,14 @@ pub struct DeserializedConfig {
pub email_listing_page_size: Option<usize>, pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>, 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_format: Option<EmailTextPlainFormat>,
pub email_reading_verify_cmd: Option<String>,
pub email_reading_decrypt_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>, 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>, pub email_hooks: Option<EmailHooks>,
#[serde(flatten)] #[serde(flatten)]
@ -41,74 +48,52 @@ pub struct DeserializedConfig {
impl DeserializedConfig { impl DeserializedConfig {
/// Tries to create a config from an optional path. /// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> { pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
trace!(">> parse config from path");
debug!("path: {:?}", path); debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?); let config: Self = match path.map(|s| s.into()).or_else(Self::path) {
let content = fs::read_to_string(path).context("cannot read config file")?; Some(path) => {
let config: Self = toml::from_str(&content).context("cannot parse config file")?; 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() { if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account")); return Err(anyhow!("config file must contain at least one account"));
} }
trace!("config: {:?}", config); trace!("config: {:#?}", config);
trace!("<< parse config from path");
Ok(config) Ok(config)
} }
/// Tries to get the XDG config file path from XDG_CONFIG_HOME /// Tries to return a config path from a few default settings.
/// environment variable. ///
fn path_from_xdg() -> Result<PathBuf> { /// Tries paths in this order:
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"); /// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
Ok(path) /// OSes.)
} /// - `"$HOME/.config/himalaya/config.toml"`
/// - `"$HOME/.himalayarc"`
/// Tries to get the XDG config file path from HOME environment ///
/// variable. /// Returns `Some(path)` if the path exists, otherwise `None`.
fn path_from_xdg_alt() -> Result<PathBuf> { pub fn path() -> Option<PathBuf> {
let home_var = if cfg!(target_family = "windows") { config_dir()
"USERPROFILE" .map(|p| p.join("himalaya").join("config.toml"))
} else { .filter(|p| p.exists())
"HOME" .or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
}; .filter(|p| p.exists())
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?; .or_else(|| home_dir().map(|p| p.join(".himalayarc")))
let path = PathBuf::from(path) .filter(|p| p.exists())
.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())
} }
pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> { 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 Some("default") | Some("") | None => self
.accounts .accounts
.iter() .iter()
.find_map(|(_, account)| { .find_map(|(name, account)| {
if account.is_default() { if account.is_default() {
Some(account) Some((name.clone(), account))
} else { } else {
None None
} }
@ -117,9 +102,12 @@ impl DeserializedConfig {
Some(name) => self Some(name) => self
.accounts .accounts
.get(name) .get(name)
.map(|account| (name.to_string(), account))
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))), .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)) Ok((account_config, backend_config))
} }

View file

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

View file

@ -1,5 +1,5 @@
use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig}; use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig};
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::path::PathBuf; use std::path::PathBuf;
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
@ -11,7 +11,7 @@ use himalaya_lib::MaildirConfig;
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig; use himalaya_lib::NotmuchConfig;
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SmtpConfig")] #[serde(remote = "SmtpConfig")]
struct SmtpConfigDef { struct SmtpConfigDef {
#[serde(rename = "smtp-host")] #[serde(rename = "smtp-host")]
@ -31,7 +31,7 @@ struct SmtpConfigDef {
} }
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "ImapConfig")] #[serde(remote = "ImapConfig")]
pub struct ImapConfigDef { pub struct ImapConfigDef {
#[serde(rename = "imap-host")] #[serde(rename = "imap-host")]
@ -57,7 +57,7 @@ pub struct ImapConfigDef {
} }
#[cfg(feature = "maildir-backend")] #[cfg(feature = "maildir-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "MaildirConfig")] #[serde(remote = "MaildirConfig")]
pub struct MaildirConfigDef { pub struct MaildirConfigDef {
#[serde(rename = "maildir-root-dir")] #[serde(rename = "maildir-root-dir")]
@ -65,40 +65,31 @@ pub struct MaildirConfigDef {
} }
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "NotmuchConfig")] #[serde(remote = "NotmuchConfig")]
pub struct NotmuchConfigDef { pub struct NotmuchConfigDef {
#[serde(rename = "notmuch-db-path")] #[serde(rename = "notmuch-db-path")]
pub db_path: PathBuf, 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")] #[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
enum EmailTextPlainFormatDef { pub enum EmailTextPlainFormatDef {
Auto, Auto,
Flowed, Flowed,
Fixed(usize), Fixed(usize),
} }
pub mod email_text_plain_format { #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
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)]
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")] #[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
pub enum EmailSenderDef { pub enum EmailSenderDef {
None, None,
@ -108,36 +99,27 @@ pub enum EmailSenderDef {
Sendmail(SendmailConfig), Sendmail(SendmailConfig),
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "SendmailConfig")] #[serde(remote = "SendmailConfig")]
pub struct SendmailConfigDef { pub struct SendmailConfigDef {
#[serde(rename = "sendmail-cmd")] #[serde(rename = "sendmail-cmd")]
cmd: String, 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 /// Represents the email hooks. Useful for doing extra email
/// processing before or after sending it. /// processing before or after sending it.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[serde(remote = "EmailHooks")] #[serde(remote = "EmailHooks")]
struct EmailHooksDef { pub struct EmailHooksDef {
/// Represents the hook called just before sending an email. /// Represents the hook called just before sending an email.
pub pre_send: Option<String>, 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. //! This module provides arguments related to the user account config.
use anyhow::Result; use anyhow::Result;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{Arg, ArgAction, ArgMatches, Command};
use log::info; use log::info;
use crate::ui::table; use crate::ui::table;
const ARG_ACCOUNT: &str = "account"; const ARG_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run";
const CMD_ACCOUNTS: &str = "accounts"; const CMD_ACCOUNTS: &str = "accounts";
const CMD_LIST: &str = "list";
const CMD_SYNC: &str = "sync";
type DryRun = bool;
/// Represents the account commands. /// Represents the account commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd { pub enum Cmd {
/// Represents the list accounts command. /// Represents the list accounts command.
List(table::args::MaxTableWidth), List(table::args::MaxTableWidth),
/// Represents the sync account command.
Sync(DryRun),
} }
/// Represents the account command matcher. /// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> { pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) { let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
info!("accounts command matched"); if let Some(m) = m.subcommand_matches(CMD_SYNC) {
let max_table_width = table::args::parse_max_width(m); info!("sync account subcommand matched");
Some(Cmd::List(max_table_width)) 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 { } else {
None None
}; };
@ -29,25 +45,50 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
Ok(cmd) Ok(cmd)
} }
/// Represents the account subcommands. /// Represents the account subcommand.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmd() -> Command {
vec![SubCommand::with_name(CMD_ACCOUNTS) Command::new(CMD_ACCOUNTS)
.aliases(&["account", "acc", "a"]) .about("Manage accounts")
.about("Lists accounts") .subcommands([
.arg(table::args::max_width())] 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 /// Represents the user account name argument. This argument allows
/// the user to select a different account than the default one. /// the user to select a different account than the default one.
pub fn arg<'a>() -> Arg<'a, 'a> { pub fn arg() -> Arg {
Arg::with_name(ARG_ACCOUNT) Arg::new(ARG_ACCOUNT)
.long("account") .long("account")
.short("a") .short('a')
.help("Selects a specific account") .help("Select a specific account by name")
.value_name("STRING") .value_name("STRING")
} }
/// Represents the user account name argument parser. /// Represents the user account name argument parser.
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> { pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
matches.value_of(ARG_ACCOUNT) 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")] #[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig; use himalaya_lib::NotmuchConfig;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use crate::config::{prelude::*, DeserializedConfig}; use crate::config::{prelude::*, DeserializedConfig};
/// Represents all existing kind of account config. /// 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")] #[serde(tag = "backend", rename_all = "snake_case")]
pub enum DeserializedAccountConfig { pub enum DeserializedAccountConfig {
None(DeserializedBaseAccountConfig), None(DeserializedBaseAccountConfig),
@ -33,25 +33,30 @@ pub enum DeserializedAccountConfig {
} }
impl 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 { match self {
DeserializedAccountConfig::None(config) => { DeserializedAccountConfig::None(config) => (
(config.to_account_config(global_config), BackendConfig::None) config.to_account_config(name, global_config),
} BackendConfig::None,
),
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => ( DeserializedAccountConfig::Imap(config) => (
config.base.to_account_config(global_config), config.base.to_account_config(name, global_config),
BackendConfig::Imap(&config.backend), BackendConfig::Imap(config.backend.clone()),
), ),
#[cfg(feature = "maildir-backend")] #[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => ( DeserializedAccountConfig::Maildir(config) => (
config.base.to_account_config(global_config), config.base.to_account_config(name, global_config),
BackendConfig::Maildir(&config.backend), BackendConfig::Maildir(config.backend.clone()),
), ),
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => ( DeserializedAccountConfig::Notmuch(config) => (
config.base.to_account_config(global_config), config.base.to_account_config(name, global_config),
BackendConfig::Notmuch(&config.backend), 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")] #[serde(rename_all = "kebab-case")]
pub struct DeserializedBaseAccountConfig { pub struct DeserializedBaseAccountConfig {
pub email: String, pub email: String,
@ -84,18 +89,25 @@ pub struct DeserializedBaseAccountConfig {
pub email_listing_page_size: Option<usize>, pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>, 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_format: Option<EmailTextPlainFormat>,
pub email_reading_verify_cmd: Option<String>,
pub email_reading_decrypt_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>, pub email_writing_encrypt_cmd: Option<String>,
#[serde(flatten, with = "EmailSenderDef")] #[serde(flatten, with = "EmailSenderDef")]
pub email_sender: EmailSender, pub email_sender: EmailSender,
#[serde(default, with = "email_hooks")] #[serde(default, with = "EmailHooksOptionDef", skip_serializing_if = "Option::is_none")]
pub email_hooks: Option<EmailHooks>, pub email_hooks: Option<EmailHooks>,
#[serde(default)]
pub sync: bool,
pub sync_dir: Option<PathBuf>,
} }
impl DeserializedBaseAccountConfig { 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 let mut folder_aliases = config
.folder_aliases .folder_aliases
.as_ref() .as_ref()
@ -109,6 +121,7 @@ impl DeserializedBaseAccountConfig {
); );
AccountConfig { AccountConfig {
name,
email: self.email.to_owned(), email: self.email.to_owned(),
display_name: self display_name: self
.display_name .display_name
@ -148,6 +161,16 @@ impl DeserializedBaseAccountConfig {
.map(ToOwned::to_owned) .map(ToOwned::to_owned)
.or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned)) .or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned))
.unwrap_or_default(), .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: self
.email_reading_decrypt_cmd .email_reading_decrypt_cmd
.as_ref() .as_ref()
@ -158,6 +181,16 @@ impl DeserializedBaseAccountConfig {
.as_ref() .as_ref()
.map(ToOwned::to_owned) .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: self
.email_writing_encrypt_cmd .email_writing_encrypt_cmd
.as_ref() .as_ref()
@ -168,6 +201,11 @@ impl DeserializedBaseAccountConfig {
.as_ref() .as_ref()
.map(ToOwned::to_owned) .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_sender: self.email_sender.to_owned(),
email_hooks: EmailHooks { email_hooks: EmailHooks {
pre_send: self pre_send: self
@ -183,11 +221,13 @@ impl DeserializedBaseAccountConfig {
}) })
.unwrap_or_default(), .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")] #[cfg(feature = "imap-backend")]
pub struct DeserializedImapAccountConfig { pub struct DeserializedImapAccountConfig {
#[serde(flatten)] #[serde(flatten)]
@ -196,7 +236,7 @@ pub struct DeserializedImapAccountConfig {
pub backend: ImapConfig, pub backend: ImapConfig,
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[cfg(feature = "maildir-backend")] #[cfg(feature = "maildir-backend")]
pub struct DeserializedMaildirAccountConfig { pub struct DeserializedMaildirAccountConfig {
#[serde(flatten)] #[serde(flatten)]
@ -205,7 +245,7 @@ pub struct DeserializedMaildirAccountConfig {
pub backend: MaildirConfig, pub backend: MaildirConfig,
} }
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)]
#[cfg(feature = "notmuch-backend")] #[cfg(feature = "notmuch-backend")]
pub struct DeserializedNotmuchAccountConfig { pub struct DeserializedNotmuchAccountConfig {
#[serde(flatten)] #[serde(flatten)]

View file

@ -3,7 +3,8 @@
//! This module gathers all account actions triggered by the CLI. //! This module gathers all account actions triggered by the CLI.
use anyhow::Result; use anyhow::Result;
use himalaya_lib::AccountConfig; use himalaya_lib::{AccountConfig, Backend, BackendSyncBuilder, BackendSyncProgressEvent};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use log::{info, trace}; use log::{info, trace};
use crate::{ use crate::{
@ -19,7 +20,7 @@ pub fn list<'a, P: Printer>(
deserialized_config: &DeserializedConfig, deserialized_config: &DeserializedConfig,
printer: &mut P, printer: &mut P,
) -> Result<()> { ) -> Result<()> {
info!(">> account list handler"); info!("entering the list accounts handler");
let accounts: Accounts = deserialized_config.accounts.iter().into(); let accounts: Accounts = deserialized_config.accounts.iter().into();
trace!("accounts: {:?}", accounts); trace!("accounts: {:?}", accounts);
@ -36,6 +37,184 @@ pub fn list<'a, P: Printer>(
Ok(()) 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)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::{AccountConfig, ImapConfig}; use himalaya_lib::{AccountConfig, ImapConfig};
@ -101,13 +280,10 @@ mod tests {
data.print_table(&mut self.writer, opts)?; data.print_table(&mut self.writer, opts)?;
Ok(()) Ok(())
} }
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> { fn print_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn print_struct<T: Debug + Print + serde::Serialize>( fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
&mut self,
_data: T,
) -> Result<()> {
unimplemented!() unimplemented!()
} }
fn is_json(&self) -> bool { 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 anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand}; use clap::{Arg, ArgAction, ArgMatches, Command};
use himalaya_lib::email::TplOverride;
use log::{debug, trace};
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_CRITERIA: &str = "criterion";
const ARG_ENCRYPT: &str = "encrypt"; const ARG_HEADERS: &str = "headers";
const ARG_HEADERS: &str = "header";
const ARG_ID: &str = "id"; const ARG_ID: &str = "id";
const ARG_IDS: &str = "ids"; const ARG_IDS: &str = "ids";
const ARG_MIME_TYPE: &str = "mime-type"; const ARG_MIME_TYPE: &str = "mime-type";
@ -24,7 +21,7 @@ const ARG_REPLY_ALL: &str = "reply-all";
const ARG_SANITIZE: &str = "sanitize"; const ARG_SANITIZE: &str = "sanitize";
const CMD_ATTACHMENTS: &str = "attachments"; const CMD_ATTACHMENTS: &str = "attachments";
const CMD_COPY: &str = "copy"; const CMD_COPY: &str = "copy";
const CMD_DEL: &str = "delete"; const CMD_DELETE: &str = "delete";
const CMD_FORWARD: &str = "forward"; const CMD_FORWARD: &str = "forward";
const CMD_LIST: &str = "list"; const CMD_LIST: &str = "list";
const CMD_MOVE: &str = "move"; const CMD_MOVE: &str = "move";
@ -36,37 +33,35 @@ const CMD_SEND: &str = "send";
const CMD_SORT: &str = "sort"; const CMD_SORT: &str = "sort";
const CMD_WRITE: &str = "write"; const CMD_WRITE: &str = "write";
type Criteria = String; pub type All = bool;
type Encrypt = bool; pub type Criteria = String;
type Folder<'a> = &'a str; pub type Folder<'a> = &'a str;
type Page = usize; pub type Headers<'a> = Vec<&'a str>;
type PageSize = usize; pub type Id<'a> = &'a str;
type Query = String; pub type Ids<'a> = Vec<&'a str>;
type Sanitize = bool; pub type Page = usize;
type Raw = bool; pub type PageSize = usize;
type RawEmail<'a> = &'a str; pub type Query = String;
type TextMime<'a> = &'a str; pub type Raw = bool;
pub type RawEmail = String;
pub(crate) type All = bool; pub type Sanitize = bool;
pub(crate) type Attachments<'a> = Vec<&'a str>; pub type TextMime<'a> = &'a str;
pub(crate) type Headers<'a> = Vec<&'a str>;
pub(crate) type Id<'a> = &'a str;
pub(crate) type Ids<'a> = &'a str;
/// Represents the email commands. /// Represents the email commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> { pub enum Cmd<'a> {
Attachments(Id<'a>), Attachments(Ids<'a>),
Copy(Id<'a>, Folder<'a>), Copy(Ids<'a>, Folder<'a>),
Delete(Ids<'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), List(table::args::MaxTableWidth, Option<PageSize>, Page),
Move(Id<'a>, Folder<'a>), Move(Ids<'a>, Folder<'a>),
Read(Id<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>), Read(Ids<'a>, TextMime<'a>, Sanitize, Raw, Headers<'a>),
Reply(Id<'a>, All, Attachments<'a>, Encrypt), Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
Save(RawEmail<'a>), Save(RawEmail),
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page), Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
Send(RawEmail<'a>), Send(RawEmail),
Sort( Sort(
Criteria, Criteria,
Query, Query,
@ -74,74 +69,61 @@ pub enum Cmd<'a> {
Option<PageSize>, Option<PageSize>,
Page, Page,
), ),
Write(TplOverride<'a>, Attachments<'a>, Encrypt),
Flag(Option<flag::args::Cmd<'a>>),
Tpl(Option<tpl::args::Cmd<'a>>), Tpl(Option<tpl::args::Cmd<'a>>),
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
} }
/// Email command matcher. /// Email command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> { 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) { let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
debug!("attachments command matched"); let ids = parse_ids_arg(m);
let id = parse_id_arg(m); Cmd::Attachments(ids)
Cmd::Attachments(id)
} else if let Some(m) = m.subcommand_matches(CMD_COPY) { } else if let Some(m) = m.subcommand_matches(CMD_COPY) {
debug!("copy command matched"); let ids = parse_ids_arg(m);
let id = parse_id_arg(m);
let folder = folder::args::parse_target_arg(m); let folder = folder::args::parse_target_arg(m);
Cmd::Copy(id, folder) Cmd::Copy(ids, folder)
} else if let Some(m) = m.subcommand_matches(CMD_DEL) { } else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
debug!("delete command matched");
let ids = parse_ids_arg(m); let ids = parse_ids_arg(m);
Cmd::Delete(ids) 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) { } else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
debug!("forward command matched");
let id = parse_id_arg(m); let id = parse_id_arg(m);
let attachments = parse_attachments_arg(m); let headers = tpl::args::parse_headers_arg(m);
let encrypt = parse_encrypt_flag(m); let body = tpl::args::parse_body_arg(m);
Cmd::Forward(id, attachments, encrypt) Cmd::Forward(id, headers, body)
} else if let Some(m) = m.subcommand_matches(CMD_LIST) { } 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 max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m); let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m); let page = parse_page_arg(m);
Cmd::List(max_table_width, page_size, page) Cmd::List(max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) { } else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
debug!("move command matched"); let ids = parse_ids_arg(m);
let id = parse_id_arg(m);
let folder = folder::args::parse_target_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) { } else if let Some(m) = m.subcommand_matches(CMD_READ) {
debug!("read command matched"); let ids = parse_ids_arg(m);
let id = parse_id_arg(m);
let mime = parse_mime_type_arg(m); let mime = parse_mime_type_arg(m);
let sanitize = parse_sanitize_flag(m); let sanitize = parse_sanitize_flag(m);
let raw = parse_raw_flag(m); let raw = parse_raw_flag(m);
let headers = parse_headers_arg(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) { } else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
debug!("reply command matched");
let id = parse_id_arg(m); let id = parse_id_arg(m);
let all = parse_reply_all_flag(m); let all = parse_reply_all_flag(m);
let attachments = parse_attachments_arg(m); let headers = tpl::args::parse_headers_arg(m);
let encrypt = parse_encrypt_flag(m); let body = tpl::args::parse_body_arg(m);
Cmd::Reply(id, all, attachments, encrypt) Cmd::Reply(id, all, headers, body)
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) { } else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
debug!("save command matched");
let email = parse_raw_arg(m); let email = parse_raw_arg(m);
Cmd::Save(email) Cmd::Save(email)
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) { } 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 max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m); let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m); let page = parse_page_arg(m);
let query = parse_query_arg(m); let query = parse_query_arg(m);
Cmd::Search(query, max_table_width, page_size, page) Cmd::Search(query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SORT) { } 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 max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m); let page_size = parse_page_size_arg(m);
let page = parse_page_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); let query = parse_query_arg(m);
Cmd::Sort(criteria, query, max_table_width, page_size, page) Cmd::Sort(criteria, query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SEND) { } else if let Some(m) = m.subcommand_matches(CMD_SEND) {
debug!("send command matched");
let email = parse_raw_arg(m); let email = parse_raw_arg(m);
Cmd::Send(email) 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) { } else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
Cmd::Tpl(tpl::args::matches(m)?) Cmd::Tpl(tpl::args::matches(m)?)
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) { } else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
Cmd::Flag(flag::args::matches(m)?) let headers = tpl::args::parse_headers_arg(m);
let body = tpl::args::parse_body_arg(m);
Cmd::Write(headers, body)
} else { } else {
debug!("default list command matched");
Cmd::List(None, None, 0) Cmd::List(None, None, 0)
}; };
@ -171,80 +147,74 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
} }
/// Represents the email subcommands. /// Represents the email subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds() -> Vec<Command> {
vec![ vec![
flag::args::subcmds(), flag::args::subcmds(),
tpl::args::subcmds(), tpl::args::subcmds(),
vec![ vec![
SubCommand::with_name(CMD_ATTACHMENTS) Command::new(CMD_ATTACHMENTS)
.aliases(&["attachment", "attach", "att", "at", "a"]) .about("Downloads all emails attachments")
.about("Downloads all attachments of the targeted email") .arg(ids_arg()),
.arg(email::args::id_arg()), Command::new(CMD_LIST)
SubCommand::with_name(CMD_LIST) .alias("lst")
.aliases(&["lst", "l"]) .about("List envelopes")
.about("Lists all emails")
.arg(page_size_arg()) .arg(page_size_arg())
.arg(page_arg()) .arg(page_arg())
.arg(table::args::max_width()), .arg(table::args::max_width()),
SubCommand::with_name(CMD_SEARCH) Command::new(CMD_SEARCH)
.aliases(&["s", "query", "q"]) .aliases(["query", "q"])
.about("Lists emails matching the given query") .about("Filter envelopes matching the given query")
.arg(page_size_arg()) .arg(page_size_arg())
.arg(page_arg()) .arg(page_arg())
.arg(table::args::max_width()) .arg(table::args::max_width())
.arg(query_arg()), .arg(query_arg()),
SubCommand::with_name(CMD_SORT) Command::new(CMD_SORT)
.about("Sorts emails by the given criteria and matching the given query") .about("Sort envelopes by the given criteria and matching the given query")
.arg(page_size_arg()) .arg(page_size_arg())
.arg(page_arg()) .arg(page_arg())
.arg(table::args::max_width()) .arg(table::args::max_width())
.arg(criteria_arg()) .arg(criteria_arg())
.arg(query_arg()), .arg(query_arg()),
SubCommand::with_name(CMD_WRITE) Command::new(CMD_WRITE)
.about("Writes a new email") .about("Write a new email")
.aliases(&["w", "new", "n"]) .aliases(["new", "n"])
.args(&tpl::args::args()) .args(tpl::args::args()),
.arg(attachments_arg()) Command::new(CMD_SEND)
.arg(encrypt_flag()), .about("Send a raw email")
SubCommand::with_name(CMD_SEND)
.about("Sends a raw email")
.arg(raw_arg()), .arg(raw_arg()),
SubCommand::with_name(CMD_SAVE) Command::new(CMD_SAVE)
.about("Saves a raw email") .about("Save a raw email")
.arg(raw_arg()), .arg(raw_arg()),
SubCommand::with_name(CMD_READ) Command::new(CMD_READ)
.about("Reads text bodies of an email") .about("Read text bodies of emails")
.arg(id_arg())
.arg(mime_type_arg()) .arg(mime_type_arg())
.arg(sanitize_flag()) .arg(sanitize_flag())
.arg(raw_flag()) .arg(raw_flag())
.arg(headers_arg()), .arg(headers_arg())
SubCommand::with_name(CMD_REPLY) .arg(ids_arg()),
.aliases(&["rep", "r"]) Command::new(CMD_REPLY)
.about("Answers to an email") .about("Answer to an email")
.arg(id_arg())
.arg(reply_all_flag()) .arg(reply_all_flag())
.arg(attachments_arg()) .args(tpl::args::args())
.arg(encrypt_flag()), .arg(id_arg()),
SubCommand::with_name(CMD_FORWARD) Command::new(CMD_FORWARD)
.aliases(&["fwd", "f"]) .aliases(["fwd", "f"])
.about("Forwards an email") .about("Forward an email")
.arg(id_arg()) .args(tpl::args::args())
.arg(attachments_arg()) .arg(id_arg()),
.arg(encrypt_flag()), Command::new(CMD_COPY)
SubCommand::with_name(CMD_COPY) .alias("cp")
.aliases(&["cp", "c"]) .about("Copy emails to the given folder")
.about("Copies an email to the targeted folder") .arg(folder::args::target_arg())
.arg(id_arg()) .arg(ids_arg()),
.arg(folder::args::target_arg()), Command::new(CMD_MOVE)
SubCommand::with_name(CMD_MOVE) .alias("mv")
.aliases(&["mv"]) .about("Move emails to the given folder")
.about("Moves an email to the targeted folder") .arg(folder::args::target_arg())
.arg(id_arg()) .arg(ids_arg()),
.arg(folder::args::target_arg()), Command::new(CMD_DELETE)
SubCommand::with_name(CMD_DEL) .aliases(["remove", "rm"])
.aliases(&["del", "d", "remove", "rm"]) .about("Delete emails")
.about("Deletes an email")
.arg(ids_arg()), .arg(ids_arg()),
], ],
] ]
@ -252,29 +222,45 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
} }
/// Represents the email id argument. /// Represents the email id argument.
pub fn id_arg<'a>() -> Arg<'a, 'a> { pub fn id_arg() -> Arg {
Arg::with_name(ARG_ID) Arg::new(ARG_ID)
.help("Specifies the target email") .help("Specifies the target email")
.value_name("ID") .value_name("ID")
.required(true) .required(true)
} }
/// Represents the email id argument parser. /// Represents the email id argument parser.
pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_id_arg(matches: &ArgMatches) -> &str {
matches.value_of(ARG_ID).unwrap() 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. /// Represents the email sort criteria argument.
pub fn criteria_arg<'a>() -> Arg<'a, 'a> { pub fn criteria_arg<'a>() -> Arg {
Arg::with_name(ARG_CRITERIA) Arg::new(ARG_CRITERIA)
.long("criterion")
.short("c")
.help("Email sorting preferences") .help("Email sorting preferences")
.long("criterion")
.short('c')
.value_name("CRITERION:ORDER") .value_name("CRITERION:ORDER")
.takes_value(true) .action(ArgAction::Append)
.multiple(true) .value_parser([
.required(true)
.possible_values(&[
"arrival", "arrival",
"arrival:asc", "arrival:asc",
"arrival:desc", "arrival:desc",
@ -300,69 +286,59 @@ pub fn criteria_arg<'a>() -> Arg<'a, 'a> {
} }
/// Represents the email sort criteria argument parser. /// 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 matches
.values_of(ARG_CRITERIA) .get_many::<String>(ARG_CRITERIA)
.unwrap_or_default() .unwrap_or_default()
.map(ToOwned::to_owned)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .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. /// Represents the email reply all argument.
pub fn reply_all_flag<'a>() -> Arg<'a, 'a> { pub fn reply_all_flag() -> Arg {
Arg::with_name(ARG_REPLY_ALL) Arg::new(ARG_REPLY_ALL)
.help("Includes all recipients") .help("Includes all recipients")
.short("A")
.long("all") .long("all")
.short('a')
.action(ArgAction::SetTrue)
} }
/// Represents the email reply all argument parser. /// Represents the email reply all argument parser.
pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
matches.is_present(ARG_REPLY_ALL) matches.get_flag(ARG_REPLY_ALL)
} }
/// Represents the page size argument. /// Represents the page size argument.
fn page_size_arg<'a>() -> Arg<'a, 'a> { fn page_size_arg() -> Arg {
Arg::with_name(ARG_PAGE_SIZE) Arg::new(ARG_PAGE_SIZE)
.help("Page size") .help("Page size")
.short("s") .long("page-size")
.long("size") .short('s')
.value_name("INT") .value_name("INT")
} }
/// Represents the page size argument parser. /// Represents the page size argument parser.
fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<usize> { fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok()) matches
.get_one::<String>(ARG_PAGE_SIZE)
.and_then(|s| s.parse().ok())
} }
/// Represents the page argument. /// Represents the page argument.
fn page_arg<'a>() -> Arg<'a, 'a> { fn page_arg() -> Arg {
Arg::with_name(ARG_PAGE) Arg::new(ARG_PAGE)
.help("Page number") .help("Page number")
.short("p") .short('p')
.long("page") .long("page")
.value_name("INT") .value_name("INT")
.default_value("1") .default_value("1")
} }
/// Represents the page argument parser. /// Represents the page argument parser.
fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize { fn parse_page_arg(matches: &ArgMatches) -> usize {
matches matches
.value_of(ARG_PAGE) .get_one::<String>(ARG_PAGE)
.unwrap() .unwrap()
.parse() .parse()
.ok() .ok()
@ -370,120 +346,94 @@ fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize {
.unwrap_or_default() .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. /// Represents the email headers argument.
pub fn headers_arg<'a>() -> Arg<'a, 'a> { pub fn headers_arg() -> Arg {
Arg::with_name(ARG_HEADERS) Arg::new(ARG_HEADERS)
.help("Shows additional headers with the email") .help("Shows additional headers with the email")
.short("h")
.long("header") .long("header")
.short('H')
.value_name("STRING") .value_name("STRING")
.multiple(true) .action(ArgAction::Append)
} }
/// Represents the email headers argument parser. /// Represents the email headers argument parser.
pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> { pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
matches.values_of(ARG_HEADERS).unwrap_or_default().collect() m.get_many::<String>(ARG_HEADERS)
.unwrap_or_default()
.map(String::as_str)
.collect::<Vec<_>>()
} }
/// Represents the sanitize flag. /// Represents the sanitize flag.
pub fn sanitize_flag<'a>() -> Arg<'a, 'a> { pub fn sanitize_flag() -> Arg {
Arg::with_name(ARG_SANITIZE) Arg::new(ARG_SANITIZE)
.help("Sanitizes text bodies") .help("Sanitizes text bodies")
.long("sanitize") .long("sanitize")
.short("s") .short('s')
.action(ArgAction::SetTrue)
} }
/// Represents the raw flag. /// Represents the raw flag.
pub fn raw_flag<'a>() -> Arg<'a, 'a> { pub fn raw_flag() -> Arg {
Arg::with_name(ARG_RAW) Arg::new(ARG_RAW)
.help("Returns raw version of email") .help("Returns raw version of email")
.long("raw") .long("raw")
.short("r") .short('r')
.action(ArgAction::SetTrue)
} }
/// Represents the sanitize flag parser. /// Represents the sanitize flag parser.
pub fn parse_sanitize_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { pub fn parse_sanitize_flag(m: &ArgMatches) -> bool {
matches.is_present(ARG_SANITIZE) m.get_flag(ARG_SANITIZE)
} }
/// Represents the raw flag parser. /// Represents the raw flag parser.
pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool { pub fn parse_raw_flag(m: &ArgMatches) -> bool {
matches.is_present(ARG_RAW) m.get_flag(ARG_RAW)
} }
/// Represents the email raw argument. /// Represents the email raw argument.
pub fn raw_arg<'a>() -> Arg<'a, 'a> { pub fn raw_arg() -> Arg {
Arg::with_name(ARG_RAW).raw(true) Arg::new(ARG_RAW).raw(true)
} }
/// Represents the email raw argument parser. /// Represents the email raw argument parser.
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_raw_arg(m: &ArgMatches) -> String {
matches.value_of(ARG_RAW).unwrap_or_default() m.get_one::<String>(ARG_RAW).cloned().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)
} }
/// Represents the email MIME type argument. /// Represents the email MIME type argument.
pub fn mime_type_arg<'a>() -> Arg<'a, 'a> { pub fn mime_type_arg() -> Arg {
Arg::with_name(ARG_MIME_TYPE) Arg::new(ARG_MIME_TYPE)
.help("MIME type to use") .help("MIME type to use")
.short("t") .short('t')
.long("mime-type") .long("mime-type")
.value_name("MIME") .value_name("MIME")
.possible_values(&["plain", "html"]) .value_parser(["plain", "html"])
.default_value("plain") .default_value("plain")
} }
/// Represents the email MIME type argument parser. /// Represents the email MIME type argument parser.
pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
matches.value_of(ARG_MIME_TYPE).unwrap() matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
} }
/// Represents the email query argument. /// Represents the email query argument.
pub fn query_arg<'a>() -> Arg<'a, 'a> { pub fn query_arg() -> Arg {
Arg::with_name(ARG_QUERY) Arg::new(ARG_QUERY)
.long_help("The query system depends on the backend, see the wiki for more details") .long_help("The query system depends on the backend, see the wiki for more details")
.value_name("QUERY") .value_name("QUERY")
.multiple(true) .num_args(1..)
.required(true) .required(true)
} }
/// Represents the email query argument parser. /// 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 matches
.values_of(ARG_QUERY) .get_many::<String>(ARG_QUERY)
.unwrap_or_default() .unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| { .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 // Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => { ("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string()); cmds.push(cmd.to_string());

View file

@ -1,123 +1,138 @@
//! Module related to message handling. use anyhow::{anyhow, Context, Result};
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream; use atty::Stream;
use himalaya_lib::{ use himalaya_lib::{
AccountConfig, Backend, Email, Part, Parts, PartsReaderOptions, Sender, TextPlainPart, AccountConfig, Backend, Email, Flag, Flags, Sender, ShowTextPartsStrategy, Tpl, TplBuilder,
TplOverride,
}; };
use log::{debug, info, trace}; use log::{debug, trace};
use mailparse::addrparse;
use std::{ use std::{
borrow::Cow,
fs, fs,
io::{self, BufRead}, io::{self, BufRead},
}; };
use url::Url; use url::Url;
use uuid::Uuid;
use crate::{ use crate::{
printer::{PrintTableOpts, Printer}, printer::{PrintTableOpts, Printer},
ui::editor, ui::editor,
}; };
/// Downloads all message attachments to the user account downloads directory. pub fn attachments<P: Printer, B: Backend + ?Sized>(
pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
ids: Vec<&str>,
) -> Result<()> { ) -> Result<()> {
let attachments = backend.email_get(mbox, seq)?.attachments(); let folder = config.folder_alias(folder)?;
let attachments_len = attachments.len(); let emails = backend.get_emails(&folder, ids.clone())?;
let mut index = 0;
if attachments_len == 0 { let mut emails_count = 0;
return printer.print_struct(format!("No attachment found for message {}", seq)); 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!( match attachments_count {
"{} attachment(s) found for message {}", 0 => printer.print("No attachment found!"),
attachments_len, seq 1 => printer.print("Downloaded 1 attachment!"),
))?; n => printer.print(format!(
"Downloaded {} attachment(s) from {} email(s)!",
for attachment in attachments { n, emails_count,
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))?;
} }
printer.print_struct("Done!")
} }
/// Copy a message from a folder to another. pub fn copy<P: Printer, B: Backend + ?Sized>(
pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
from_folder: &str,
to_folder: &str,
ids: Vec<&str>,
) -> Result<()> { ) -> Result<()> {
backend.email_copy(mbox_src, mbox_dst, seq)?; let from_folder = config.folder_alias(from_folder)?;
printer.print_struct(format!( let to_folder = config.folder_alias(to_folder)?;
"Message {} successfully copied to folder {}", backend.copy_emails(&from_folder, &to_folder, ids)?;
seq, mbox_dst printer.print("Email(s) successfully copied!")
))
} }
/// Delete messages matching the given sequence range. pub fn delete<P: Printer, B: Backend + ?Sized>(
pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
seq: &str,
mbox: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
ids: Vec<&str>,
) -> Result<()> { ) -> Result<()> {
backend.email_delete(mbox, seq)?; let folder = config.folder_alias(folder)?;
printer.print_struct(format!("Message(s) {} successfully deleted", seq)) backend.delete_emails(&folder, ids)?;
printer.print("Email(s) successfully deleted!")
} }
/// Forward the given message UID from the selected folder. pub fn forward<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
seq: &str,
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
sender: &mut S, sender: &mut S,
folder: &str,
id: &str,
headers: Option<Vec<&str>>,
body: Option<&str>,
) -> Result<()> { ) -> Result<()> {
let msg = backend let folder = config.folder_alias(folder)?;
.email_get(mbox, seq)? let tpl = backend
.into_forward(config)? .get_emails(&folder, vec![id])?
.add_attachments(attachments_paths)? .first()
.encrypt(encrypt); .ok_or_else(|| anyhow!("cannot find email {}", id))?
editor::edit_email_with_editor( .to_forward_tpl_builder(config)?
msg, .set_some_raw_headers(headers)
TplOverride::default(), .some_text_plain_part(body)
config, .build();
printer, trace!("initial template: {}", *tpl);
backend, editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?;
sender,
)?;
Ok(()) Ok(())
} }
/// List paginated messages from the selected folder. pub fn list<P: Printer, B: Backend + ?Sized>(
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
) -> Result<()> { ) -> Result<()> {
let folder = config.folder_alias(folder)?;
let page_size = page_size.unwrap_or(config.email_listing_page_size()); let page_size = page_size.unwrap_or(config.email_listing_page_size());
debug!("page size: {}", 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); trace!("envelopes: {:?}", msgs);
printer.print_table( printer.print_table(
Box::new(msgs), 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. /// Parses and edits a message from a [mailto] URL string.
/// ///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto /// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( pub fn mailto<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
url: &Url,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
sender: &mut S, sender: &mut S,
url: &Url,
) -> Result<()> { ) -> Result<()> {
info!("entering mailto command handler"); let mut tpl = TplBuilder::default().to(url.path());
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();
for (key, val) in url.query_pairs() { for (key, val) in url.query_pairs() {
match key.as_bytes() { match key.to_lowercase().as_bytes() {
b"cc" => { b"cc" => tpl = tpl.cc(val),
cc.push(val.to_string()); b"bcc" => tpl = tpl.bcc(val),
} b"subject" => tpl = tpl.subject(val),
b"bcc" => { b"body" => tpl = tpl.text_plain_part(val.as_bytes()),
bcc.push(val.to_string());
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (), _ => (),
} }
} }
let msg = Email { editor::edit_tpl_with_editor(config, printer, backend, sender, tpl.build())
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(())
} }
/// Move a message from a folder to another. pub fn move_<P: Printer, B: Backend + ?Sized>(
pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
from_folder: &str,
to_folder: &str,
ids: Vec<&str>,
) -> Result<()> { ) -> Result<()> {
backend.email_move(mbox_src, mbox_dst, seq)?; let from_folder = config.folder_alias(from_folder)?;
printer.print_struct(format!( let to_folder = config.folder_alias(to_folder)?;
r#"Message {} successfully moved to folder "{}""#, backend.move_emails(&from_folder, &to_folder, ids)?;
seq, mbox_dst printer.print("Email(s) successfully moved!")
))
} }
/// Read a message by its sequence number. pub fn read<P: Printer, B: Backend + ?Sized>(
pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
seq: &str, printer: &mut P,
backend: &mut B,
folder: &str,
ids: Vec<&str>,
text_mime: &str, text_mime: &str,
sanitize: bool, sanitize: bool,
raw: bool, raw: bool,
headers: Vec<&str>, 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, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
) -> Result<()> { sender: &mut S,
let msg = backend.email_get(mbox, seq)?; folder: &str,
id: &str,
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,
all: bool, all: bool,
attachments_paths: Vec<&str>, headers: Option<Vec<&str>>,
encrypt: bool, body: Option<&str>,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: &mut B,
sender: &mut S,
) -> Result<()> { ) -> Result<()> {
let msg = backend let folder = config.folder_alias(folder)?;
.email_get(mbox, seq)? let tpl = backend
.into_reply(all, config)? .get_emails(&folder, vec![id])?
.add_attachments(attachments_paths)? .first()
.encrypt(encrypt); .ok_or_else(|| anyhow!("cannot find email {}", id))?
editor::edit_email_with_editor( .to_reply_tpl_builder(config, all)?
msg, .set_some_raw_headers(headers)
TplOverride::default(), .some_text_plain_part(body)
config, .build();
printer, trace!("initial template: {}", *tpl);
backend, editor::edit_tpl_with_editor(config, printer, backend, sender, tpl)?;
sender, backend.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))?;
)?;
backend.flags_add(mbox, seq, "replied")?;
Ok(()) Ok(())
} }
/// Saves a raw message to the targetted folder. pub fn save<P: Printer, B: Backend + ?Sized>(
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
mbox: &str,
raw_msg: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
raw_email: String,
) -> Result<()> { ) -> Result<()> {
debug!("folder: {}", mbox); let folder = config.folder_alias(folder)?;
let is_tty = atty::is(Stream::Stdin); let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json(); 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 { let raw_email = if is_tty || is_json {
raw_email.replace("\r", "").replace("\n", "\r\n") raw_email.replace("\r", "").replace("\n", "\r\n")
} else { } else {
@ -377,26 +275,92 @@ pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\r\n") .join("\r\n")
}; };
trace!("raw message: {:?}", raw_email); backend.add_email(&folder, raw_email.as_bytes(), &Flags::default())?;
let email = Email::from_tpl(&raw_email)?;
sender.send(&email)?;
backend.email_add(&sent_folder, raw_email.as_bytes(), "seen")?;
Ok(()) Ok(())
} }
/// Compose a new message. pub fn search<P: Printer, B: Backend + ?Sized>(
pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( config: &AccountConfig,
tpl: TplOverride, printer: &mut P,
attachments_paths: Vec<&str>, backend: &mut B,
encrypt: bool, 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, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
sender: &mut S, sender: &mut S,
raw_email: String,
) -> Result<()> { ) -> Result<()> {
let email = Email::default() let folder = config.folder_alias("sent")?;
.add_attachments(attachments_paths)? let is_tty = atty::is(Stream::Stdin);
.encrypt(encrypt); let is_json = printer.is_json();
editor::edit_email_with_editor(email, tpl, config, printer, backend, sender)?; 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(()) Ok(())
} }

View file

@ -8,7 +8,7 @@ impl Table for Envelope {
.cell(Cell::new("ID").bold().underline().white()) .cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white()) .cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().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()) .cell(Cell::new("DATE").bold().underline().white())
} }
@ -17,8 +17,12 @@ impl Table for Envelope {
let flags = self.flags.to_symbols_string(); let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen); let unseen = !self.flags.contains(&Flag::Seen);
let subject = &self.subject; let subject = &self.subject;
let sender = &self.sender; let sender = if let Some(name) = &self.from.name {
let date = self.date.as_deref().unwrap_or_default(); name
} else {
&self.from.addr
};
let date = self.date.format("%d/%m/%Y %H:%M").to_string();
Row::new() Row::new()
.cell(Cell::new(id).bold_if(unseen).red()) .cell(Cell::new(id).bold_if(unseen).red())

View file

@ -1,10 +1,11 @@
//! Email flag CLI module. //! Email flag CLI module.
//! //!
//! This module provides subcommands, arguments and a command matcher //! This module contains the command matcher, the subcommands and the
//! related to the email flag domain. //! arguments related to the email flag domain.
use anyhow::Result; 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 log::{debug, info};
use crate::email; use crate::email;
@ -12,38 +13,36 @@ use crate::email;
const ARG_FLAGS: &str = "flag"; const ARG_FLAGS: &str = "flag";
const CMD_ADD: &str = "add"; const CMD_ADD: &str = "add";
const CMD_DEL: &str = "remove"; const CMD_REMOVE: &str = "remove";
const CMD_SET: &str = "set"; const CMD_SET: &str = "set";
pub(crate) const CMD_FLAG: &str = "flag"; pub(crate) const CMD_FLAG: &str = "flags";
type Flags = String;
/// Represents the flag commands. /// Represents the flag commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> { pub enum Cmd<'a> {
Add(email::args::Ids<'a>, Flags), Add(email::args::Ids<'a>, Flags),
Remove(email::args::Ids<'a>, Flags),
Set(email::args::Ids<'a>, Flags), Set(email::args::Ids<'a>, Flags),
Del(email::args::Ids<'a>, Flags),
} }
/// Represents the flag command matcher. /// Represents the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) { 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 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)) 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) { } 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 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)) 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 { } else {
None None
}; };
@ -52,48 +51,51 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
} }
/// Represents the flag subcommands. /// Represents the flag subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<Command> {
vec![SubCommand::with_name(CMD_FLAG) vec![Command::new(CMD_FLAG)
.aliases(&["flags", "flg"])
.about("Handles email flags") .about("Handles email flags")
.setting(AppSettings::SubcommandRequiredElseHelp) .subcommand_required(true)
.arg_required_else_help(true)
.subcommand( .subcommand(
SubCommand::with_name(CMD_ADD) Command::new(CMD_ADD)
.aliases(&["a"]) .about("Adds flags to an email")
.about("Adds email flags")
.arg(email::args::ids_arg()) .arg(email::args::ids_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name(CMD_SET) Command::new(CMD_REMOVE)
.aliases(&["s", "change", "c"]) .aliases(["delete", "del", "d"])
.about("Sets email flags") .about("Removes flags from an email")
.arg(email::args::ids_arg()) .arg(email::args::ids_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name(CMD_DEL) Command::new(CMD_SET)
.aliases(&["rem", "rm", "r", "delete", "del", "d"]) .aliases(["change", "c"])
.about("Removes email flags") .about("Sets flags of an email")
.arg(email::args::ids_arg()) .arg(email::args::ids_arg())
.arg(flags_arg()), .arg(flags_arg()),
)] )]
} }
/// Represents the flags argument. /// Represents the flags argument.
pub fn flags_arg<'a>() -> Arg<'a, 'a> { pub fn flags_arg() -> Arg {
Arg::with_name(ARG_FLAGS) Arg::new(ARG_FLAGS)
.long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.") .value_name("FLAGS")
.value_name("FLAGS…") .help("The flags")
.multiple(true) .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) .required(true)
.last(true)
} }
/// Represents the flags argument parser. /// Represents the flags argument parser.
pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String { pub fn parse_flags_arg(matches: &ArgMatches) -> Flags {
matches Flags::from_iter(
.values_of(ARG_FLAGS) matches
.unwrap_or_default() .get_many::<String>(ARG_FLAGS)
.collect::<Vec<_>>() .unwrap_or_default()
.join(" ") .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 anyhow::Result;
use himalaya_lib::backend::Backend; use himalaya_lib::{Backend, Flags};
use crate::printer::Printer; use crate::printer::Printer;
/// Adds flags to all messages matching the given sequence range. pub fn add<P: Printer, B: Backend + ?Sized>(
/// 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,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> { ) -> Result<()> {
backend.flags_add(mbox, seq_range, flags)?; backend.add_flags(folder, ids, flags)?;
printer.print_struct(format!( printer.print("Flag(s) successfully added!")
"Flag(s) {:?} successfully added to message(s) {:?}",
flags, seq_range
))
} }
/// Removes flags from all messages matching the given sequence range. pub fn set<P: Printer, B: Backend + ?Sized>(
/// 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,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> { ) -> Result<()> {
backend.flags_delete(mbox, seq_range, flags)?; backend.set_flags(folder, ids, flags)?;
printer.print_struct(format!( printer.print("Flag(s) successfully set!")
"Flag(s) {:?} successfully removed from message(s) {:?}",
flags, seq_range
))
} }
/// Replaces flags of all messages matching the given sequence range. pub fn remove<P: Printer, B: Backend + ?Sized>(
/// 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,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> { ) -> Result<()> {
backend.flags_set(mbox, seq_range, flags)?; backend.remove_flags(folder, ids, flags)?;
printer.print_struct(format!( printer.print("Flag(s) successfully removed!")
"Flag(s) {:?} successfully set for message(s) {:?}",
flags, seq_range
))
} }

View file

@ -4,7 +4,7 @@
//! related to the folder domain. //! related to the folder domain.
use anyhow::Result; use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand}; use clap::{self, Arg, ArgMatches, Command};
use log::debug; use log::debug;
use crate::ui::table; use crate::ui::table;
@ -32,122 +32,108 @@ pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
Ok(cmd) Ok(cmd)
} }
/// Represents folder subcommands. /// Represents the folder subcommand.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmd() -> Command {
vec![SubCommand::with_name(CMD_FOLDERS) Command::new(CMD_FOLDERS)
.aliases(&[
"folder",
"fold",
"fo",
"mailboxes",
"mailbox",
"mboxes",
"mbox",
"mb",
"m",
])
.about("Lists folders") .about("Lists folders")
.arg(table::args::max_width())] .arg(table::args::max_width())
} }
/// Represents the source folder argument. /// Represents the source folder argument.
pub fn source_arg<'a>() -> Arg<'a, 'a> { pub fn source_arg() -> Arg {
Arg::with_name(ARG_SOURCE) Arg::new(ARG_SOURCE)
.short("f")
.long("folder") .long("folder")
.short('f')
.help("Specifies the source folder") .help("Specifies the source folder")
.value_name("SOURCE") .value_name("SOURCE")
.default_value("inbox") .default_value("inbox")
} }
/// Represents the source folder argument parser. /// Represents the source folder argument parser.
pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_source_arg(matches: &ArgMatches) -> &str {
matches.value_of(ARG_SOURCE).unwrap() matches.get_one::<String>(ARG_SOURCE).unwrap().as_str()
} }
/// Represents the target folder argument. /// Represents the target folder argument.
pub fn target_arg<'a>() -> Arg<'a, 'a> { pub fn target_arg() -> Arg {
Arg::with_name(ARG_TARGET) Arg::new(ARG_TARGET)
.help("Specifies the target folder") .help("Specifies the target folder")
.value_name("TARGET") .value_name("TARGET")
.required(true) .required(true)
} }
/// Represents the target folder argument parser. /// Represents the target folder argument parser.
pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_target_arg(matches: &ArgMatches) -> &str {
matches.value_of(ARG_TARGET).unwrap() matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use clap::{App, ErrorKind}; use clap::{error::ErrorKind, Command};
use super::*; use super::*;
#[test] #[test]
fn it_should_match_cmds() { fn it_should_match_cmds() {
let arg = App::new("himalaya") let arg = Command::new("himalaya")
.subcommands(subcmds()) .subcommand(subcmd())
.get_matches_from(&["himalaya", "folders"]); .get_matches_from(&["himalaya", "folders"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap()); assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
let arg = App::new("himalaya") let arg = Command::new("himalaya")
.subcommands(subcmds()) .subcommand(subcmd())
.get_matches_from(&["himalaya", "folders", "--max-width", "20"]); .get_matches_from(&["himalaya", "folders", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap()); 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] #[test]
fn it_should_match_source_arg() { fn it_should_match_source_arg() {
macro_rules! get_matches_from { macro_rules! get_matches_from {
($($arg:expr),*) => { ($($arg:expr),*) => {
App::new("himalaya") Command::new("himalaya")
.arg(source_arg()) .arg(source_arg())
.get_matches_from(&["himalaya", $($arg,)*]) .get_matches_from(&["himalaya", $($arg,)*])
}; };
} }
let app = get_matches_from![]; 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"]; 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"]; 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] #[test]
fn it_should_match_target_arg() { fn it_should_match_target_arg() {
macro_rules! get_matches_from { macro_rules! get_matches_from {
($($arg:expr),*) => { ($($arg:expr),*) => {
App::new("himalaya") Command::new("himalaya")
.arg(target_arg()) .arg(target_arg())
.get_matches_from_safe(&["himalaya", $($arg,)*]) .try_get_matches_from_mut(&["himalaya", $($arg,)*])
}; };
} }
let app = get_matches_from![]; 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"]; 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 anyhow::Result;
use himalaya_lib::{AccountConfig, Backend}; use himalaya_lib::{AccountConfig, Backend};
use log::trace;
use crate::printer::{PrintTableOpts, Printer}; use crate::printer::{PrintTableOpts, Printer};
/// Lists all folders. pub fn list<P: Printer, B: Backend + ?Sized>(
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
max_width: Option<usize>, max_width: Option<usize>,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
) -> Result<()> { ) -> Result<()> {
let folders = backend.folder_list()?; let folders = backend.list_folders()?;
trace!("folders: {:?}", folders);
printer.print_table( printer.print_table(
// TODO: remove Box // TODO: remove Box
Box::new(folders), Box::new(folders),
@ -29,8 +26,10 @@ pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders}; use himalaya_lib::{
use std::{fmt::Debug, io}; backend, AccountConfig, Backend, Emails, Envelope, Envelopes, Flags, Folder, Folders,
};
use std::{any::Any, fmt::Debug, io};
use termcolor::ColorSpec; use termcolor::ColorSpec;
use crate::printer::{Print, PrintTable, WriteColor}; use crate::printer::{Print, PrintTable, WriteColor};
@ -87,10 +86,10 @@ mod tests {
data.print_table(&mut self.writer, opts)?; data.print_table(&mut self.writer, opts)?;
Ok(()) 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!() unimplemented!()
} }
fn print_struct<T: Debug + Print + serde::Serialize>( fn print<T: Debug + Print + serde::Serialize>(
&mut self, &mut self,
_data: T, _data: T,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@ -103,12 +102,15 @@ mod tests {
struct TestBackend; struct TestBackend;
impl<'a> Backend<'a> for TestBackend { impl Backend for TestBackend {
fn folder_add(&mut self, _: &str) -> backend::Result<()> { fn name(&self) -> String {
unimplemented!(); unimplemented!();
} }
fn folder_list(&mut self) -> backend::Result<Folders> { fn add_folder(&self, _: &str) -> backend::Result<()> {
Ok(Folders(vec![ unimplemented!();
}
fn list_folders(&self) -> backend::Result<Folders> {
Ok(Folders::from_iter([
Folder { Folder {
delim: "/".into(), delim: "/".into(),
name: "INBOX".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!(); 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!() unimplemented!()
} }
fn envelope_search( fn search_envelopes(
&mut self, &self,
_: &str, _: &str,
_: &str, _: &str,
_: &str, _: &str,
@ -137,31 +148,63 @@ mod tests {
) -> backend::Result<Envelopes> { ) -> backend::Result<Envelopes> {
unimplemented!() unimplemented!()
} }
fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> { fn add_email(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result<String> {
unimplemented!() unimplemented!()
} }
fn email_get(&mut self, _: &str, _: &str) -> backend::Result<Email> { fn add_email_internal(&self, _: &str, _: &[u8], _: &Flags) -> backend::Result<String> {
unimplemented!() unimplemented!()
} }
fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { fn get_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
unimplemented!() unimplemented!()
} }
fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { fn preview_emails(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
unimplemented!() unimplemented!()
} }
fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> { fn get_emails_internal(&self, _: &str, _: Vec<&str>) -> backend::Result<Emails> {
unimplemented!() unimplemented!()
} }
fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { fn copy_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
unimplemented!() unimplemented!()
} }
fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { fn copy_emails_internal(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
unimplemented!() unimplemented!()
} }
fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { fn move_emails(&self, _: &str, _: &str, _: Vec<&str>) -> backend::Result<()> {
unimplemented!() 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 self
} }
} }

View file

@ -3,64 +3,60 @@
//! This module provides subcommands and a command matcher related to IMAP. //! This module provides subcommands and a command matcher related to IMAP.
use anyhow::Result; use anyhow::Result;
use clap::{App, ArgMatches}; use clap::{value_parser, Arg, ArgMatches, Command};
use log::{debug, info}; use log::debug;
const ARG_KEEPALIVE: &str = "keepalive";
const CMD_NOTIFY: &str = "notify";
const CMD_WATCH: &str = "watch";
type Keepalive = u64; type Keepalive = u64;
/// IMAP commands. /// IMAP commands.
pub enum Command { pub enum Cmd {
/// Start the IMAP notify mode with the give keepalive duration. /// Start the IMAP notify mode with the give keepalive duration.
Notify(Keepalive), Notify(Keepalive),
/// Start the IMAP watch mode with the give keepalive duration. /// Start the IMAP watch mode with the give keepalive duration.
Watch(Keepalive), Watch(Keepalive),
} }
/// IMAP command matcher. /// IMAP command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> { pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
info!("entering imap command matcher"); if let Some(m) = m.subcommand_matches(CMD_NOTIFY) {
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
if let Some(m) = m.subcommand_matches("notify") {
info!("notify command matched");
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive); debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Notify(keepalive))); return Ok(Some(Cmd::Notify(*keepalive)));
} }
if let Some(m) = m.subcommand_matches("watch") { if let Some(m) = m.subcommand_matches(CMD_WATCH) {
info!("watch command matched"); let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive); debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Watch(keepalive))); return Ok(Some(Cmd::Watch(*keepalive)));
} }
Ok(None) Ok(None)
} }
/// IMAP subcommands. /// IMAP subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<Command> {
vec![ vec![
clap::SubCommand::with_name("notify") Command::new(CMD_NOTIFY)
.about("Notifies when new messages arrive in the given folder") .about("Notifies when new messages arrive in the given folder")
.aliases(&["idle"]) .alias("idle")
.arg( .arg(keepalive_arg()),
clap::Arg::with_name("keepalive") Command::new(CMD_WATCH)
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
clap::SubCommand::with_name("watch")
.about("Watches IMAP server changes") .about("Watches IMAP server changes")
.arg( .arg(keepalive_arg()),
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
] ]
} }
/// 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 anyhow::{Context, Result};
use himalaya_lib::ImapBackend; use himalaya_lib::ImapBackend;
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { pub fn notify(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
imap.notify(keepalive, mbox).context("cannot imap notify") imap.notify(keepalive, folder).context("cannot imap notify")
} }
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { pub fn watch(imap: &ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
imap.watch(keepalive, mbox).context("cannot imap watch") imap.watch(keepalive, folder).context("cannot imap watch")
} }

View file

@ -4,68 +4,58 @@
//! related to email templating. //! related to email templating.
use anyhow::Result; use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{Arg, ArgAction, ArgMatches, Command};
use himalaya_lib::email::TplOverride;
use log::debug;
use crate::email; use crate::email;
const ARG_BCC: &str = "bcc";
const ARG_BODY: &str = "body"; const ARG_BODY: &str = "body";
const ARG_CC: &str = "cc"; const ARG_HEADERS: &str = "headers";
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_TPL: &str = "template"; const ARG_TPL: &str = "template";
const CMD_FORWARD: &str = "forward"; const CMD_FORWARD: &str = "forward";
const CMD_NEW: &str = "new";
const CMD_REPLY: &str = "reply"; const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save"; const CMD_SAVE: &str = "save";
const CMD_SEND: &str = "send"; 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. /// Represents the template commands.
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> { pub enum Cmd<'a> {
Forward(email::args::Id<'a>, TplOverride<'a>), Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
New(TplOverride<'a>), Write(Headers<'a>, Body<'a>),
Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>), Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
Save(email::args::Attachments<'a>, Tpl<'a>), Save(RawTpl),
Send(email::args::Attachments<'a>, Tpl<'a>), Send(RawTpl),
} }
/// Represents the template command matcher. /// Represents the template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) { let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
debug!("forward subcommand matched");
let id = email::args::parse_id_arg(m); let id = email::args::parse_id_arg(m);
let tpl = parse_override_arg(m); let headers = parse_headers_arg(m);
Some(Cmd::Forward(id, tpl)) let body = parse_body_arg(m);
} else if let Some(m) = m.subcommand_matches(CMD_NEW) { Some(Cmd::Forward(id, headers, body))
debug!("new subcommand matched");
let tpl = parse_override_arg(m);
Some(Cmd::New(tpl))
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) { } else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
debug!("reply subcommand matched");
let id = email::args::parse_id_arg(m); let id = email::args::parse_id_arg(m);
let all = email::args::parse_reply_all_flag(m); let all = email::args::parse_reply_all_flag(m);
let tpl = parse_override_arg(m); let headers = parse_headers_arg(m);
Some(Cmd::Reply(id, all, tpl)) let body = parse_body_arg(m);
Some(Cmd::Reply(id, all, headers, body))
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) { } else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
debug!("save subcommand matched"); let raw_tpl = parse_raw_arg(m);
let attachments = email::args::parse_attachments_arg(m); Some(Cmd::Save(raw_tpl))
let tpl = parse_raw_arg(m);
Some(Cmd::Save(attachments, tpl))
} else if let Some(m) = m.subcommand_matches(CMD_SEND) { } else if let Some(m) = m.subcommand_matches(CMD_SEND) {
debug!("send subcommand matched"); let raw_tpl = parse_raw_arg(m);
let attachments = email::args::parse_attachments_arg(m); Some(Cmd::Send(raw_tpl))
let tpl = parse_raw_arg(m); } else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
Some(Cmd::Send(attachments, tpl)) let headers = parse_headers_arg(m);
let body = parse_body_arg(m);
Some(Cmd::Write(headers, body))
} else { } else {
None None
}; };
@ -74,112 +64,76 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
} }
/// Represents the template subcommands. /// Represents the template subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<Command> {
vec![SubCommand::with_name(CMD_TPL) vec![Command::new(CMD_TPL)
.aliases(&["tpl"]) .alias("tpl")
.about("Handles email templates") .about("Handles email templates")
.setting(AppSettings::SubcommandRequiredElseHelp) .subcommand_required(true)
.arg_required_else_help(true)
.subcommand( .subcommand(
SubCommand::with_name(CMD_NEW) Command::new(CMD_FORWARD)
.aliases(&["n"]) .alias("fwd")
.about("Generates a template for a new email") .about("Generates a template for forwarding an email")
.arg(email::args::id_arg())
.args(&args()), .args(&args()),
) )
.subcommand( .subcommand(
SubCommand::with_name(CMD_REPLY) Command::new(CMD_REPLY)
.aliases(&["rep", "re", "r"])
.about("Generates a template for replying to an email") .about("Generates a template for replying to an email")
.arg(email::args::id_arg()) .arg(email::args::id_arg())
.arg(email::args::reply_all_flag()) .arg(email::args::reply_all_flag())
.args(&args()), .args(&args()),
) )
.subcommand( .subcommand(
SubCommand::with_name(CMD_FORWARD) Command::new(CMD_SAVE)
.aliases(&["fwd", "fw", "f"]) .about("Compiles the template into a valid email then saves it")
.about("Generates a template for forwarding an email") .arg(Arg::new(ARG_TPL).raw(true)),
.arg(email::args::id_arg()) )
.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()), .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. /// Represents the template arguments.
pub fn args<'a>() -> Vec<Arg<'a, 'a>> { pub fn args() -> Vec<Arg> {
vec![ vec![
Arg::with_name(ARG_SUBJECT) Arg::new(ARG_HEADERS)
.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)
.help("Overrides a specific header") .help("Overrides a specific header")
.short("h") .short('H')
.long("header") .long("header")
.value_name("KEY:VAL") .value_name("KEY:VAL")
.multiple(true), .action(ArgAction::Append),
Arg::with_name(ARG_BODY) Arg::new(ARG_BODY)
.help("Overrides the body") .help("Overrides the body")
.short("B") .short('B')
.long("body") .long("body")
.value_name("STRING"), .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. /// Represents the template headers argument parser.
pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride { pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> {
TplOverride { m.get_many(ARG_HEADERS)
subject: matches.value_of(ARG_SUBJECT), .map(|h| h.map(String::as_str).collect::<Vec<_>>())
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), /// Represents the template body argument parser.
bcc: matches.values_of(ARG_BCC).map(Iterator::collect), pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> {
headers: matches.values_of(ARG_HEADERS).map(Iterator::collect), matches.get_one::<String>(ARG_BODY).map(String::as_str)
body: matches.value_of(ARG_BODY),
signature: matches.value_of(ARG_SIGNATURE),
}
} }
/// Represents the raw template argument parser. /// Represents the raw template argument parser.
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str { pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl {
matches.value_of(ARG_TPL).unwrap_or_default() matches
.get_one::<String>(ARG_TPL)
.cloned()
.unwrap_or_default()
} }

View file

@ -1,103 +1,119 @@
//! Module related to message template handling. use anyhow::{anyhow, Result};
//!
//! This module gathers all message template commands.
use anyhow::Result;
use atty::Stream; use atty::Stream;
use himalaya_lib::{AccountConfig, Backend, Email, Sender, TplOverride}; use himalaya_lib::{AccountConfig, Backend, CompilerBuilder, Email, Flags, Sender, Tpl};
use std::io::{self, BufRead}; use std::io::{stdin, BufRead};
use crate::printer::Printer; use crate::printer::Printer;
/// Generate a new message template. pub fn forward<P: Printer, B: Backend + ?Sized>(
pub fn new<'a, P: Printer>( config: &AccountConfig,
opts: TplOverride<'a>, printer: &mut P,
config: &'a AccountConfig, backend: &mut B,
printer: &'a mut P, folder: &str,
id: &str,
headers: Option<Vec<&str>>,
body: Option<&str>,
) -> Result<()> { ) -> Result<()> {
let tpl = Email::default().to_tpl(opts, config)?; let tpl = backend
printer.print_struct(tpl) .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<P: Printer, B: Backend + ?Sized>(
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized>( config: &AccountConfig,
seq: &str, printer: &mut P,
backend: &mut B,
folder: &str,
id: &str,
all: bool, all: bool,
opts: TplOverride<'_>, headers: Option<Vec<&str>>,
mbox: &str, body: Option<&str>,
config: &AccountConfig,
printer: &mut P,
backend: &mut B,
) -> Result<()> { ) -> Result<()> {
let tpl = backend let tpl = backend
.email_get(mbox, seq)? .get_emails(folder, vec![id])?
.into_reply(all, config)? .first()
.to_tpl(opts, config)?; .ok_or_else(|| anyhow!("cannot find email {}", id))?
printer.print_struct(tpl) .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 save<P: Printer, B: Backend + ?Sized>(
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized>(
seq: &str,
opts: TplOverride<'_>,
mbox: &str,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
folder: &str,
tpl: String,
) -> Result<()> { ) -> Result<()> {
let tpl = backend let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
.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() {
tpl.replace("\r", "") tpl.replace("\r", "")
} else { } else {
io::stdin() stdin()
.lock() .lock()
.lines() .lines()
.filter_map(Result::ok) .filter_map(Result::ok)
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
}; })
let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?; .compile(
let raw_email = email.into_sendable(config)?.formatted(); CompilerBuilder::default()
backend.email_add(mbox, &raw_email, "seen")?; .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
printer.print_struct("Template successfully saved") .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<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( config: &AccountConfig,
mbox: &str,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
sender: &mut S, sender: &mut S,
folder: &str,
tpl: String,
) -> Result<()> { ) -> 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", "") tpl.replace("\r", "")
} else { } else {
io::stdin() stdin()
.lock() .lock()
.lines() .lines()
.filter_map(Result::ok) .filter_map(Result::ok)
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join("\n") .join("\n")
}; })
let email = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?; .compile(
let sent_msg = sender.send(&email)?; CompilerBuilder::default()
backend.email_add(mbox, &sent_msg, "seen")?; .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
printer.print_struct("Template successfully sent") .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 compl;
pub mod config; pub mod config;
pub mod domain; pub mod domain;
pub mod man;
pub mod output; pub mod output;
pub mod printer; pub mod printer;
pub mod ui; pub mod ui;

View file

@ -1,12 +1,12 @@
use anyhow::{Context, Result}; use anyhow::Result;
use std::env; use clap::Command;
use std::{borrow::Cow, env};
use url::Url; use url::Url;
use himalaya::{ use himalaya::{
account, compl, account, cache, compl,
config::{self, DeserializedConfig}, config::{self, DeserializedConfig},
email, flag, folder, email, flag, folder, man, output,
output::{self, OutputFmt},
printer::StdoutPrinter, printer::StdoutPrinter,
tpl, tpl,
}; };
@ -15,19 +15,22 @@ use himalaya_lib::{BackendBuilder, BackendConfig, ImapBackend, SenderBuilder};
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
use himalaya::imap; use himalaya::imap;
fn create_app<'a>() -> clap::App<'a, 'a> { fn create_app() -> Command {
let app = clap::App::new(env!("CARGO_PKG_NAME")) let app = Command::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION")) .version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION")) .about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS")) .author(env!("CARGO_PKG_AUTHORS"))
.global_setting(clap::AppSettings::GlobalVersion) .propagate_version(true)
.arg(&config::args::arg()) .infer_subcommands(true)
.arg(&account::args::arg()) .arg(config::args::arg())
.args(&output::args::args()) .arg(account::args::arg())
.arg(cache::args::arg())
.args(output::args::args())
.arg(folder::args::source_arg()) .arg(folder::args::source_arg())
.subcommands(compl::args::subcmds()) .subcommand(compl::args::subcmd())
.subcommands(account::args::subcmds()) .subcommand(man::args::subcmd())
.subcommands(folder::args::subcmds()) .subcommand(account::args::subcmd())
.subcommand(folder::args::subcmd())
.subcommands(email::args::subcmds()); .subcommands(email::args::subcmds());
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
@ -41,73 +44,99 @@ fn main() -> Result<()> {
let default_env_filter = env_logger::DEFAULT_FILTER_ENV; let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); 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(); let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let url = Url::parse(&raw_args[1])?; let url = Url::parse(&raw_args[1])?;
let config = DeserializedConfig::from_opt_path(None)?; let config = DeserializedConfig::from_opt_path(None)?;
let (account_config, backend_config) = config.to_configs(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 sender = SenderBuilder::build(&account_config)?;
let mut printer = StdoutPrinter::from_fmt(OutputFmt::Plain); let mut printer = StdoutPrinter::default();
return email::handlers::mailto( return email::handlers::mailto(
&url,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
sender.as_mut(), sender.as_mut(),
&url,
); );
} }
let app = create_app(); let app = create_app();
let m = app.get_matches(); let m = app.get_matches();
// Check completion command BEFORE entities and services initialization. // checks completion command before configs
// Related issue: https://github.com/soywod/himalaya/issues/115. // https://github.com/soywod/himalaya/issues/115
match compl::args::matches(&m)? { match compl::args::matches(&m)? {
Some(compl::args::Command::Generate(shell)) => { Some(compl::args::Cmd::Generate(shell)) => {
return compl::handlers::generate(create_app(), 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 config = DeserializedConfig::from_opt_path(config::args::parse_arg(&m))?;
let (account_config, backend_config) = config.to_configs(account::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))?; let folder = account_config.folder_alias(folder::args::parse_source_arg(&m))?;
// Check IMAP commands. // checks IMAP commands
#[cfg(feature = "imap-backend")] #[cfg(feature = "imap-backend")]
if let BackendConfig::Imap(imap_config) = backend_config { if let BackendConfig::Imap(imap_config) = &backend_config {
// FIXME: find a way to downcast `backend` instead. // FIXME: find a way to downcast `backend` instead of
let mut imap = ImapBackend::new(&account_config, imap_config); // recreating an instance.
match imap::args::matches(&m)? { match imap::args::matches(&m)? {
Some(imap::args::Command::Notify(keepalive)) => { Some(imap::args::Cmd::Notify(keepalive)) => {
return imap::handlers::notify(keepalive, &folder, &mut imap); 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)) => { Some(imap::args::Cmd::Watch(keepalive)) => {
return imap::handlers::watch(keepalive, &folder, &mut imap); 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 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)? { match account::args::matches(&m)? {
Some(account::args::Cmd::List(max_width)) => { Some(account::args::Cmd::List(max_width)) => {
return account::handlers::list(max_width, &account_config, &config, &mut printer); 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)? { match folder::args::matches(&m)? {
Some(folder::args::Cmd::List(max_width)) => { 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( return folder::handlers::list(
max_width, max_width,
&account_config, &account_config,
@ -118,202 +147,270 @@ fn main() -> Result<()> {
_ => (), _ => (),
} }
// Check message commands. // checks email commands
match email::args::matches(&m)? { 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( return email::handlers::attachments(
seq,
&folder,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
&folder,
ids,
); );
} }
Some(email::args::Cmd::Copy(seq, mbox_dst)) => { Some(email::args::Cmd::Copy(ids, to_folder)) => {
return email::handlers::copy(seq, &folder, mbox_dst, &mut printer, backend.as_mut()); let mut backend = BackendBuilder::new()
} .disable_cache(disable_cache)
Some(email::args::Cmd::Delete(seq)) => { .build(&account_config, &backend_config)?;
return email::handlers::delete(seq, &folder, &mut printer, backend.as_mut()); return email::handlers::copy(
} &account_config,
Some(email::args::Cmd::Forward(seq, attachment_paths, encrypt)) => { &mut printer,
return email::handlers::forward( backend.as_mut(),
seq,
attachment_paths,
encrypt,
&folder, &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, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
sender.as_mut(), sender.as_mut(),
&folder,
id,
headers,
body,
); );
} }
Some(email::args::Cmd::List(max_width, page_size, page)) => { 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( return email::handlers::list(
max_width,
page_size,
page,
&folder,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
&folder,
max_width,
page_size,
page,
); );
} }
Some(email::args::Cmd::Move(seq, mbox_dst)) => { Some(email::args::Cmd::Move(ids, to_folder)) => {
return email::handlers::move_(seq, &folder, mbox_dst, &mut printer, backend.as_mut()); 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( return email::handlers::read(
seq, &account_config,
&mut printer,
backend.as_mut(),
&folder,
ids,
text_mime, text_mime,
sanitize, sanitize,
raw, raw,
headers, 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( return email::handlers::reply(
seq,
all,
attachment_paths,
encrypt,
&folder,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
sender.as_mut(), sender.as_mut(),
&folder,
id,
all,
headers,
body,
); );
} }
Some(email::args::Cmd::Save(raw_msg)) => { Some(email::args::Cmd::Save(raw_email)) => {
return email::handlers::save(&folder, raw_msg, &mut printer, backend.as_mut()); 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)) => { 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( return email::handlers::search(
&account_config,
&mut printer,
backend.as_mut(),
&folder,
query, query,
max_width, max_width,
page_size, page_size,
page, page,
&folder,
&account_config,
&mut printer,
backend.as_mut(),
); );
} }
Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { 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( return email::handlers::sort(
&account_config,
&mut printer,
backend.as_mut(),
&folder,
criteria, criteria,
query, query,
max_width, max_width,
page_size, page_size,
page, 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( 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, &account_config,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
sender.as_mut(), sender.as_mut(),
raw_email,
); );
} }
Some(email::args::Cmd::Flag(m)) => match m { Some(email::args::Cmd::Flag(m)) => match m {
Some(flag::args::Cmd::Set(seq_range, ref flags)) => { Some(flag::args::Cmd::Set(ids, ref flags)) => {
return flag::handlers::set( let mut backend = BackendBuilder::new()
seq_range, .disable_cache(disable_cache)
flags, .build(&account_config, &backend_config)?;
&folder, return flag::handlers::set(&mut printer, backend.as_mut(), &folder, ids, flags);
&mut printer,
backend.as_mut(),
);
} }
Some(flag::args::Cmd::Add(seq_range, ref flags)) => { Some(flag::args::Cmd::Add(ids, ref flags)) => {
return flag::handlers::add( let mut backend = BackendBuilder::new()
seq_range, .disable_cache(disable_cache)
flags, .build(&account_config, &backend_config)?;
&folder, return flag::handlers::add(&mut printer, backend.as_mut(), &folder, ids, flags);
&mut printer,
backend.as_mut(),
);
} }
Some(flag::args::Cmd::Del(seq_range, ref flags)) => { Some(flag::args::Cmd::Remove(ids, ref flags)) => {
return flag::handlers::remove( let mut backend = BackendBuilder::new()
seq_range, .disable_cache(disable_cache)
flags, .build(&account_config, &backend_config)?;
&folder, return flag::handlers::remove(&mut printer, backend.as_mut(), &folder, ids, flags);
&mut printer,
backend.as_mut(),
);
} }
_ => (), _ => (),
}, },
Some(email::args::Cmd::Tpl(m)) => match m { Some(email::args::Cmd::Tpl(m)) => match m {
Some(tpl::args::Cmd::New(tpl)) => { Some(tpl::args::Cmd::Forward(id, headers, body)) => {
return tpl::handlers::new(tpl, &account_config, &mut printer); let mut backend = BackendBuilder::new()
} .disable_cache(disable_cache)
Some(tpl::args::Cmd::Reply(seq, all, tpl)) => { .build(&account_config, &backend_config)?;
return tpl::handlers::reply(
seq,
all,
tpl,
&folder,
&account_config,
&mut printer,
backend.as_mut(),
);
}
Some(tpl::args::Cmd::Forward(seq, tpl)) => {
return tpl::handlers::forward( return tpl::handlers::forward(
seq,
tpl,
&folder,
&account_config, &account_config,
&mut printer, &mut printer,
backend.as_mut(), 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( return tpl::handlers::save(
&folder,
&account_config, &account_config,
atts,
tpl,
&mut printer, &mut printer,
backend.as_mut(), 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( return tpl::handlers::send(
&folder, &account_config,
atts,
tpl,
&mut printer, &mut printer,
backend.as_mut(), backend.as_mut(),
sender.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; use clap::Arg;
pub(crate) const ARG_COLOR: &str = "color";
pub(crate) const ARG_OUTPUT: &str = "output";
/// Output arguments. /// Output arguments.
pub fn args<'a>() -> Vec<Arg<'a, 'a>> { pub fn args() -> Vec<Arg> {
vec![ vec![
Arg::with_name("output") Arg::new(ARG_OUTPUT)
.long("output")
.short("o")
.help("Defines the output format") .help("Defines the output format")
.long("output")
.short('o')
.value_name("FMT") .value_name("FMT")
.possible_values(&["plain", "json"]) .value_parser(["plain", "json"])
.default_value("plain"), .default_value("plain"),
Arg::with_name("log-level") Arg::new(ARG_COLOR)
.long("log-level") .help("Controls when to use colors.")
.alias("log") .long_help(
.short("l") "
.help("Defines the logs level") This flag controls when to use colors. The default setting is 'auto', which
.value_name("LEVEL") means himalaya will try to guess when to use colors. For example, if himalaya is
.possible_values(&["error", "warn", "info", "debug", "trace"]) printing to a terminal, then it will use colors, but if it is redirected to a
.default_value("info"), 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 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. /// Represents the available output formats.
#[derive(Debug, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutputFmt { pub enum OutputFmt {
Plain, Plain,
Json, Json,
} }
impl From<&str> for OutputFmt { impl Default for OutputFmt {
fn from(fmt: &str) -> Self { fn default() -> Self {
match fmt { Self::Plain
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
_ => Self::Plain,
}
} }
} }
impl TryFrom<Option<&str>> for OutputFmt { impl FromStr for OutputFmt {
type Error = Error; type Err = Error;
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> { fn from_str(fmt: &str) -> Result<Self, Self::Err> {
match fmt { match fmt {
Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json), fmt if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain), fmt if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
None => Ok(Self::Plain), unknown => Err(anyhow!("cannot parse output format {}", unknown)),
Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)),
} }
} }
} }
@ -39,3 +38,76 @@ impl fmt::Display for OutputFmt {
write!(f, "{}", fmt) 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 anyhow::{Context, Result};
use himalaya_lib::Tpl;
use crate::printer::WriteColor; use crate::printer::WriteColor;
@ -19,3 +20,10 @@ impl Print for String {
Ok(writer.reset()?) 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 anyhow::{Context, Error, Result};
use atty::Stream; use clap::ArgMatches;
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
use termcolor::{ColorChoice, StandardStream}; use termcolor::StandardStream;
use crate::{ use crate::{
output::OutputFmt, output::{args, ColorFmt, OutputFmt},
printer::{Print, PrintTable, PrintTableOpts, WriteColor}, printer::{Print, PrintTable, PrintTableOpts, WriteColor},
}; };
pub trait Printer { pub trait Printer {
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()>; fn print<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn print_struct<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>( fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self, &mut self,
data: Box<T>, data: Box<T>,
@ -24,41 +24,30 @@ pub struct StdoutPrinter {
pub fmt: OutputFmt, pub fmt: OutputFmt,
} }
impl StdoutPrinter { impl Default for StdoutPrinter {
pub fn from_fmt(fmt: OutputFmt) -> Self { fn default() -> Self {
let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { let fmt = OutputFmt::default();
// Colors should be deactivated if the terminal is not a tty. let writer = Box::new(StandardStream::stdout(ColorFmt::default().into()));
ColorChoice::Never Self { fmt, writer }
} 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 }
} }
}
pub fn from_opt_str(s: Option<&str>) -> Result<Self> { impl StdoutPrinter {
Ok(Self { pub fn new(fmt: OutputFmt, color: ColorFmt) -> Self {
fmt: OutputFmt::try_from(s)?, let writer = Box::new(StandardStream::stdout(color.into()));
..Self::from_fmt(OutputFmt::Plain) Self { fmt, writer }
})
} }
} }
impl Printer for StdoutPrinter { 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 { match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()), OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => Ok(()), 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 { match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()), OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data) OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &data)
@ -86,3 +75,29 @@ impl Printer for StdoutPrinter {
self.fmt == OutputFmt::Json 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 anyhow::{Context, Result};
use himalaya_lib::{ use himalaya_lib::{
email::{local_draft_path, remove_local_draft, Email, TplOverride}, email::{local_draft_path, remove_local_draft},
AccountConfig, Backend, Sender, AccountConfig, Backend, CompilerBuilder, Flag, Flags, Sender, Tpl,
}; };
use log::{debug, info}; use log::debug;
use std::{env, fs, process::Command}; use std::{env, fs, process::Command};
use crate::{ use crate::{
@ -11,7 +11,7 @@ use crate::{
ui::choice::{self, PostEditChoice, PreEditChoice}, 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(); let path = local_draft_path();
debug!("create draft"); debug!("create draft");
@ -27,48 +27,34 @@ pub fn open_with_tpl(tpl: String) -> Result<String> {
let content = let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; 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 path = local_draft_path();
let tpl = let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?; 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( pub fn edit_tpl_with_editor<P: Printer, B: Backend + ?Sized, S: Sender + ?Sized>(
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,
config: &AccountConfig, config: &AccountConfig,
printer: &mut P, printer: &mut P,
backend: &mut B, backend: &mut B,
sender: &mut S, sender: &mut S,
mut tpl: Tpl,
) -> Result<()> { ) -> Result<()> {
info!("start editing with editor");
let draft = local_draft_path(); let draft = local_draft_path();
if draft.exists() { if draft.exists() {
loop { loop {
match choice::pre_edit() { match choice::pre_edit() {
Ok(choice) => match choice { Ok(choice) => match choice {
PreEditChoice::Edit => { PreEditChoice::Edit => {
let tpl = open_with_draft()?; tpl = open_with_local_draft()?;
email.merge_with(Email::from_tpl(&tpl)?);
break; break;
} }
PreEditChoice::Discard => { PreEditChoice::Discard => {
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); tpl = open_with_tpl(tpl)?;
break; break;
} }
PreEditChoice::Quit => return Ok(()), PreEditChoice::Quit => return Ok(()),
@ -80,35 +66,44 @@ pub fn edit_email_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender
} }
} }
} else { } else {
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); tpl = open_with_tpl(tpl)?;
} }
loop { loop {
match choice::post_edit() { match choice::post_edit() {
Ok(PostEditChoice::Send) => { Ok(PostEditChoice::Send) => {
printer.print_str("Sending email…")?; printer.print_log("Sending email…")?;
let sent_email: Vec<u8> = sender.send(&email)?; let email = tpl.compile(
let sent_folder = config.folder_alias("sent")?; CompilerBuilder::default()
printer.print_str(format!("Adding email to the {:?} folder…", sent_folder))?; .some_pgp_sign_cmd(config.email_writing_sign_cmd.as_ref())
backend.email_add(&sent_folder, &sent_email, "seen")?; .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()?; remove_local_draft()?;
printer.print_struct("Done!")?; printer.print("Done!")?;
break; break;
} }
Ok(PostEditChoice::Edit) => { Ok(PostEditChoice::Edit) => {
email.merge_with(_edit_email_with_editor(&email, tpl.clone(), config)?); tpl = open_with_tpl(tpl)?;
continue; continue;
} }
Ok(PostEditChoice::LocalDraft) => { Ok(PostEditChoice::LocalDraft) => {
printer.print_struct("Email successfully saved locally")?; printer.print("Email successfully saved locally")?;
break; break;
} }
Ok(PostEditChoice::RemoteDraft) => { Ok(PostEditChoice::RemoteDraft) => {
let tpl = email.to_tpl(TplOverride::default(), config)?;
let draft_folder = config.folder_alias("drafts")?; 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()?; remove_local_draft()?;
printer.print_struct(format!("Email successfully saved to {}", draft_folder))?; printer.print(format!("Email successfully saved to {}", draft_folder))?;
break; break;
} }
Ok(PostEditChoice::Discard) => { Ok(PostEditChoice::Discard) => {

View file

@ -5,17 +5,17 @@ const ARG_MAX_TABLE_WIDTH: &str = "max-table-width";
pub(crate) type MaxTableWidth = Option<usize>; pub(crate) type MaxTableWidth = Option<usize>;
/// Represents the max table width argument. /// Represents the max table width argument.
pub fn max_width<'a>() -> Arg<'a, 'a> { pub fn max_width() -> Arg {
Arg::with_name(ARG_MAX_TABLE_WIDTH) Arg::new(ARG_MAX_TABLE_WIDTH)
.help("Defines a maximum width for the table") .help("Defines a maximum width for the table")
.short("w")
.long("max-width") .long("max-width")
.short('w')
.value_name("INT") .value_name("INT")
} }
/// Represents the max table width argument parser. /// 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 matches
.value_of(ARG_MAX_TABLE_WIDTH) .get_one::<String>(ARG_MAX_TABLE_WIDTH)
.and_then(|width| width.parse::<usize>().ok()) .and_then(|s| s.parse().ok())
} }

View file

@ -183,7 +183,7 @@ where
table[0].0.iter().map(|cell| cell.unicode_width()).collect(); table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend( table.extend(
items items
.iter() .into_iter()
.map(|item| { .map(|item| {
let row = item.row(); let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| { row.0.iter().enumerate().for_each(|(i, cell)| {