Compare commits

..

551 commits

Author SHA1 Message Date
Guillaume Ranquet
bd461fa820 docs: fix example in custom_compose_hooks
example uses embed whereas the keyword should be embedded_pty

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-12-09 16:45:16 +01:00
Guillaume Ranquet
2870624d44 contacts: add notmuch address book support
Support importing notmuch address book through a query.

example query to import all contacts in the past 6 Months:

notmuch_address_book_query = "--output=recipients --deduplicate=address date:6M.."

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-12-09 17:01:55 +02:00
Manos Pitsidianakis
30c599ab92
Makefile: fix some minor logic/UX issues
- Don't hardcode printf path, use `command -v printf` instead
- Print commands that use shell expansion in env variables with `echo`
  before executing them. This will show to the user calling the make
  target the exact evaluated command that will run.
- Fix some variables not being detected/printed properly in `make help`
  output
- Add RUSTFLAGS, CARGO_TARGET_DIR to `make help` output

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
c34465bab5
docs: add more doc comments to utilities::widgets mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
1f6471556e
docs: add more doc comments to conf mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 13:24:43 +02:00
Manos Pitsidianakis
0cd0864fa1
docs: add more doc comments to version_migrations mod
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-08 11:30:06 +02:00
Manos Pitsidianakis
9f7dcfc21e
Bump version to 0.8.10
Highlights:
  ===========

  - added pipe-attachment command
  - added sample scripts for using meli as a mailto scheme handler in
    contrib/
  - fixed GPG encryption with libgpgme

  Contributors in alphabetical order:
  ===================================

  - Manos Pitsidianakis
  - Matthias Geiger

  Added
  =====

  - 5e77821f mail/view: add pipe-attachment command in PR #540
    "mail/view: add pipe-attachment command"
  - fa896f6b contrib: add mailto: scheme handler scripts
  - 00ce9660
    melib/backends: add as_any/as_any_mut methods to BackendMailbox
  - fd243fa5 maildir: add mailbox creation tests
  - de65eec3 meli/accounts: add mailbox_by_path() tests in PR #535
    "Rework maildir mailbox path logic, add tests"
  - 6b363601 melib/gpgme: impl Display for gpgme::Key

  Bug Fixes
  =========

  - 60c90d75 melib/attachments: ensure MIME boundary prefixed with CRLF
  - 3433c5c3 compose/pgp: rewrite key selection logic in PR #541 "More
    gpgme/PGP fixes again"
  - 12de82e7 melib/conf: fix mutt_alias_file not being validated in PR
    #550 "Remove sealed_test dependency"
  - c8e055a7 Fix version migrations being triggered backwards in PR #557
    "Fix version migrations being triggered backwards"
  - efab99fd
    terminal: check for NO_COLOR env var without unicode validation
  - 36a63e88 melib/maildir: rewrite create_mailbox()
  - fcab855f view: ensure envelope headers are always populated in PR
    #538 "view: ensure envelope headers are always populated"
  - 84564f44 mailcap: don't drop File before opening it in PR #552
    "mailcap: don't drop File before opening it"

  Changes
  =======

  - ed85da51 Remove sealed_test dependency

  Refactoring
  ===========

  - 03df2ac1 meli/utilities: add print utilities for tests
  - 18e9d5c1 conf.rs: impl From<melib::AccountSettings> for AccountConf
  - 1f2fec19 Fix 1.83.0 lints in PR #536 "CI: Add action to check for
    DCO signoffs in PRs"
  - 192ecea2 compose/gpg.rs: Fix msrv regression

  Documentation
  =============

  - 4a61a4b8 melib: include README.md as preamble of crate rustdocs
  - 80e53471 BUILD.md: move melib specific stuff to melib/README.md
  - 91a17ece melib/README.md: mention sqlite3-static feature
  - b77a691b meli/README.md: Add cargo features section in PR #549
    "Document cargo features in READMEs"
  - 91dc271d contrib: add a README.md file
  - 2e900be6 contrib/README.md: add section about oauth2.py
  - 07812d2c contrib/README.md: elaborate a bit about mailto in PR #545
    "Add external mailto: handler support via scripts in contrib"
  - e784e8d2 scripts: add markdown_doc_lints.py

  Continuous Integration
  ======================

  - 77629851 CI: Add action to check for DCO signoffs in PRs
  - f944ebed CI: Add error msg when cargo-derivefmt check fails
  - d49344f9 CI: Move MSRV checks from manifest to lints in PR #553
    "ci-workflow-fixes"
  - ece6bfc2 CI: non-zero exit if cargo-derivefmt-* targets fail
  - 2257b91b CI: add actions/cache steps in PR #554 "CI: add
    actions/cache steps"
  - a1c9524f CI: fix check_dco.sh not working with other repos in PR
    #555 "CI: fix check_dco.sh not working with other repos"

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-06 07:56:00 +02:00
Manos Pitsidianakis
c8e055a718
Fix version migrations being triggered backwards
Fixes #539 ("Ensure versions migrations are suggested only when current
version is newer than the previous one")

Resolves: <https://git.meli-email.org/meli/meli/issues/539>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 21:09:38 +02:00
Manos Pitsidianakis
84564f44a3
mailcap: don't drop File before opening it
Created temporary file was incorrectly set to be deleted on Drop, but
this is not what we want to do when spawning an external command and pass
the file as the argument.

Fixes: #548 ("mailcap handling")

Resolves: <https://git.meli-email.org/meli/meli/issues/548>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 19:52:32 +02:00
Manos Pitsidianakis
a1c9524f74
CI: fix check_dco.sh not working with other repos
Fixes #546

Resolves: <https://git.meli-email.org/meli/meli/issues/546>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 18:04:40 +02:00
Manos Pitsidianakis
2257b91b40
CI: add actions/cache steps
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 15:51:48 +02:00
Manos Pitsidianakis
ece6bfc2ce
CI: non-zero exit if cargo-derivefmt-* targets fail
A previous commit made a change to show an error message and a diff if
cargo-derivefmt check failed, but did not exit with a non-zero code,
thus allowing the failed check to pass.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 15:33:21 +02:00
Manos Pitsidianakis
d49344f9d8
CI: Move MSRV checks from manifest to lints
While MSRV is defined in the manifest files, it doesn't make sense to
verify the code compiles under the MSRV only when the manifest files
change. Code changes can break MSRV compliance.

Move the MSRV checks to the lints.yaml workflow instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 12:22:11 +02:00
Manos Pitsidianakis
f944ebed81
CI: Add error msg when cargo-derivefmt check fails
If cargo-derivefmt fails, no relevant message shows up on CI, only
something like this:

  cargo-derivefmt-melib
  cargo derivefmt --manifest-path ./melib/Cargo.toml
  git checkout meli/src/conf/overrides.rs
  Updated 1 path from the index
  git add --update ./melib/ && git diff --quiet && git diff --cached --quiet
  make: *** [.gitea/Makefile.lint:33: cargo-derivefmt-melib] Error 1

Now it shows a helpful error message with a diff:

  cargo-derivefmt-melib
  Checking that derives are sorted alphabetically...
  cargo derivefmt --manifest-path ./melib/Cargo.toml
  Some derives in the ./melib crate are not sorted alphabetically, see diff:
  diff --git a/melib/src/contacts/notmuchcontact.rs b/melib/src/contacts/notmuchcontact.rs
  index 4ee18f1a..bfcbf54a 100644
  --- a/melib/src/contacts/notmuchcontact.rs
  +++ b/melib/src/contacts/notmuchcontact.rs
  @@ -22,7 +22,7 @@

   use crate::{contacts::Card, error::Result};

  -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
  +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
   pub struct NotMuchContact {
       pub name: String,
       pub address: String,

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 12:22:11 +02:00
Manos Pitsidianakis
07812d2c85
contrib/README.md: elaborate a bit about mailto
Elaborate on the necessary steps to make the mailto scheme handler
scripts work.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 12:08:13 +02:00
Manos Pitsidianakis
2e900be698
contrib/README.md: add section about oauth2.py
Add section explaining what the oauth2.py script is for.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 12:08:00 +02:00
Manos Pitsidianakis
91dc271d74
contrib: add a README.md file
Add a README.md file for the entire contrib/ directory, and merge
existing `mailto-readme.md` into it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-05 11:53:17 +02:00
Matthias Geiger
fa896f6bb9
contrib: add mailto: scheme handler scripts
Signed-off-by: Matthias Geiger <werdahias@debian.org>
2024-12-05 11:50:07 +02:00
Manos Pitsidianakis
b77a691b7d
meli/README.md: Add cargo features section
Add section with a table explaining each cargo feature, like we do in
melib/README.md

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:52:22 +02:00
Manos Pitsidianakis
91a17ece5c
melib/README.md: mention sqlite3-static feature
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:52:22 +02:00
Manos Pitsidianakis
e784e8d239
scripts: add markdown_doc_lints.py
This script is meant for lints to run against our markdown files
(README.md, BUILD.md maybe even CHANGELOG.md someday).

For now it checks that a README.md file documents all cargo features,
sample output:

  epilys ~meli % python3 scripts/markdown_doc_lints.py melib/README.md melib/Cargo.toml
  The following features are not mentioned in melib/README.md

  - sqlite3-static add the following string in the Features section: <a name="sqlite3-static-feature">`sqlite3-static`</a>

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:52:22 +02:00
Manos Pitsidianakis
80e5347178
BUILD.md: move melib specific stuff to melib/README.md
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:52:21 +02:00
Manos Pitsidianakis
4a61a4b857
melib: include README.md as preamble of crate rustdocs
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:52:21 +02:00
Manos Pitsidianakis
12de82e7b4
melib/conf: fix mutt_alias_file not being validated
`mutt_alias_file` was not removed from the `extras` field of
`AccountSettings` in the `validate_config` function, thus if you used it
in your account configuration you'd get an error: 'Configuration error:
Unrecognised configuration values: {"mutt_alias_file": "<value>"}'

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:37:33 +02:00
Manos Pitsidianakis
192ecea2a4
compose/gpg.rs: Fix msrv regression
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 18:19:32 +02:00
Manos Pitsidianakis
ed85da51df
Remove sealed_test dependency
We're not using its features it provides over using just rusty-fork,
which sealed-test uses its own fork of it (rusty-forkfork) under the
hood.

Revert to just using the original rusty-fork crate, which is also
packaged in debian.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-12-04 17:38:49 +02:00
Manos Pitsidianakis
3433c5c3d5
compose/pgp: rewrite key selection logic
Rewrite key selection logic when opening the key selector widget for
signing or encryption keys:

- Fix encrypt_for_self incorrectly using the To: header email instead of
  From: (ugh!)
- Include all recipient addresses in selection (To:, Cc: Bcc:), also
  From:
- Use all addresses in a header value instead of just the first address
- Show shortcuts to sign/encrypt fields in Compose form

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-30 17:39:26 +02:00
Manos Pitsidianakis
6b3636013c
melib/gpgme: impl Display for gpgme::Key
Implement display to show more info on the user interface other than
just the fingerprint.

Will show the fingerprint, the primary user id and a list of properties/flags:

- revoked status
- expired status
- disabled status
- invalid status
- can encrypt status
- can sign status
- whether it's a secret or a public key

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-30 17:35:50 +02:00
Manos Pitsidianakis
60c90d7549
melib/attachments: ensure MIME boundary prefixed with CRLF
Sometimes attachment bytes don't end with CRLF, so we must prefix the
boundary line ourselves.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-30 17:35:50 +02:00
Manos Pitsidianakis
5e77821f78
mail/view: add pipe-attachment command
You can pipe individual attachments to binaries with the following
     command:

     pipe-attachment INDEX binary ARGS

     Example usage with the less(1) pager:
           pipe-attachment 0 less
     If the binary does not wait for your input before exiting, you will
     probably not see its output since you will return back to the user
     interface immediately.  You can write a wrapper script that pipes your
     binary's output to
           less
     or
           less -r
     if you want to preserve the ANSI escape codes in the pager's output.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-30 11:25:31 +02:00
Manos Pitsidianakis
fcab855fda
view: ensure envelope headers are always populated
Some email backends return Envelopes with only the basic headers filled
in. But that might clash with a user's configurations which operates on
specific headers when viewing an email. So, when having loaded an email
completely, make sure its envelope's headers is completely populated
before presenting it to the user.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-29 23:27:20 +02:00
Manos Pitsidianakis
1f2fec198f
Fix 1.83.0 lints
Mostly lifetime ones (elided-named-lifetimes and needless-lifetimes)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 18:01:59 +02:00
Manos Pitsidianakis
776298511b
CI: Add action to check for DCO signoffs in PRs
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 17:29:19 +02:00
Manos Pitsidianakis
de65eec3a9
meli/accounts: add mailbox_by_path() tests
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 14:19:10 +02:00
Manos Pitsidianakis
18e9d5c148
conf.rs: impl From<melib::AccountSettings> for AccountConf
No functional changes.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:11 +02:00
Manos Pitsidianakis
fd243fa5ab
maildir: add mailbox creation tests
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:11 +02:00
Manos Pitsidianakis
36a63e8878
melib/maildir: rewrite create_mailbox()
Rewrite create_mailbox() to better detect errors, and make its behavior
consistent across different scenarios (being given a file system path or
not, being given absolute paths or not, having a valid (maildir) root
mailbox or not)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:10 +02:00
Manos Pitsidianakis
00ce9660ef
melib/backends: add as_any/as_any_mut methods to BackendMailbox
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:10 +02:00
Manos Pitsidianakis
03df2ac128
meli/utilities: add print utilities for tests
Add:

- eprint_step_fn():  Returns a closure that prints a formatted message,
  without a trailing newline, and prefixed with an increasing counter.
- eprintln_ok_fn(): Returns a closure that prints the string " OK\n" to
  `stderr` with optional color formatting.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:10 +02:00
Manos Pitsidianakis
efab99fda2
terminal: check for NO_COLOR env var without unicode validation
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-28 12:44:10 +02:00
Manos Pitsidianakis
ce53925f36
Bump version to 0.8.9
This is mostly a fixups release.

Added
=====

- cf16bf65 meli/sqlite3: add tests for reindexing
- a389772d accounts: suggest tips on mailbox_by_path error

Bug Fixes
=========

- 25f0a3f8 conf/terminal: fix serde of ProgressSpinnerSequence
- c375b48e terminal: fix Synchronized Output response parsed as input
  in PR #523 "terminal: fix Synchronized Output response parsed as
  input"
- b7e215f9
  melib/utils: fix test_fd_locks() on platforms without OFD support in
  PR #524 "melib/utils: fix test_fd_locks() on platforms without OFD
  support"
- 25c32a6b meli/docs/meli.conf.examples.5: fix .Dt macro arguments
- 18ae5848 meli: fix reindex of previously indexed account with sqlite3
  backend
- 13e917d9 Fix some compilation errors with cfg feature attrs in PR #531
  "accounts: suggest tips on mailbox_by_path error"
- 8c176d38 contacts/editor: fix crash on saving contact in PR #532
  "contacts/editor: fix crash on saving contact"
- fb5a88c2
  melib/collection: ensure mailbox exists when inserting new envelopes
  in PR #529 "Small account stuff fixes"

Changes
=======

- 7f8f1cf6 melib/gpgme bindings renewal in PR #533 "melib/gpgme
  bindings renewal"
- 9b7825bc Update futures-util dep, remove stderrlog dep
- 4be69360 Remove obsolete "encoding" dependency in PR #530
  "Remove/update obsolete dependencies"

Refactoring
===========

- 5af6e059 meli/accounts: use Arc<str> for account name
- 567270e1 melib: use Vec instead of SmallVec for search results
- 2bd8d7ba
  conf/tests.rs: Rename test functions to follow path convention

Documentation
=============

- 97242482 meli/docs: add meli.conf.examples to CLI and tests
- 0f096338 README.md: Update ways to install, add gitlab mirror link
  in PR #528 "Integrate meli.conf.examples.5 into CLI and build, also
  update README with installation instructions"

Continuous Integration
======================

- 630df308 CI: Add arm64 runners in job matrices in PR #527 "CI: Add
  arm64 runners in job matrices"
- 49ecbb56 CI: .gitea/Makefile.lint: check if nightly exists

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-27 16:16:29 +02:00
Manos Pitsidianakis
7dae430759
version_migrations: add compile-time checks
Add compile-time checks that version map is sorted.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-27 16:12:31 +02:00
Manos Pitsidianakis
a6df350843
v0.8.9: add version migrations and tests for upcoming version
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-27 14:27:54 +02:00
Manos Pitsidianakis
7f8f1cf65f
melib/gpgme bindings renewal
Regenerate libgpgme bindings for 32bit and 64bit targets separately.

This fixes the gpgme bindings tests failing for 32bit targets (which
prior to this commit where cfg'd to run only on x86-64 with #[cfg(target_arch = "x86_64")]

Tests confirm passing with:

    cross test --target i686-unknown-linux-gnu --all-targets --all -- --skip test_cli_subcommands

Note: We don't currently test on 32bit arches on CI, failure was spotted
on downstream Debian's build servers

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 19:35:05 +02:00
Manos Pitsidianakis
4be6936026
Remove obsolete "encoding" dependency
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 18:54:48 +02:00
Manos Pitsidianakis
9b7825bc59
Update futures-util dep, remove stderrlog dep
stderrlog was not used at all, so remove it.

Also update yanked futures-util dep.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 18:54:48 +02:00
Manos Pitsidianakis
8c176d3840
contacts/editor: fix crash on saving contact
Oops...

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 17:52:44 +02:00
Manos Pitsidianakis
13e917d97b
Fix some compilation errors with cfg feature attrs
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 16:44:02 +02:00
Manos Pitsidianakis
49ecbb56f7
CI: .gitea/Makefile.lint: check if nightly exists
If nightly toolchain exists and `cargo +nightly fmt --check --all` runs
but fails because --check returns an error, and the same error does not
trigger under the current active toolchain when the `|| cargo fmt
--check --all` path is executed, then that error is completely ignored.

That wouldn't happen on the CI where the nightly toolchain is not
installed but it would happen if someone (i.e. me) used the Makefiles
locally.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 14:12:56 +02:00
Manos Pitsidianakis
cf16bf65f7
meli/sqlite3: add tests for reindexing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 12:19:15 +02:00
Manos Pitsidianakis
18ae584836
meli: fix reindex of previously indexed account with sqlite3 backend
Reported-by: iooioio <meli@iooioio.xyz>
Co-authored-by: iooioio <meli@iooioio.xyz>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-26 12:18:40 +02:00
Manos Pitsidianakis
a389772d96
accounts: suggest tips on mailbox_by_path error
If Account::mailbox_by_path() fails, suggest matching mailbox paths
using aho_corasick case insensitive matching, and also suggest to the
user to inspect mailboxes with the manage-mailboxes command.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 13:57:11 +02:00
Manos Pitsidianakis
0f09633899
README.md: Update ways to install, add gitlab mirror link
- Add repology table/badge
- Add more ways to install thanks to people packaging meli for package
  managers/platforms
- Update list of official mirrors by adding new gitlab mirror
- While at it, fix ugly html table with quickstart instructions

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:56:24 +02:00
Manos Pitsidianakis
972424829c
meli/docs: add meli.conf.examples to CLI and tests
We kinda forgot to do that when adding that new manpage, do it now.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:56:23 +02:00
Manos Pitsidianakis
25c32a6b95
meli/docs/meli.conf.examples.5: fix .Dt macro arguments
Dt macro takes two arguments, or three of the last one is the host
machine architecture.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:56:07 +02:00
Manos Pitsidianakis
fb5a88c22c
melib/collection: ensure mailbox exists when inserting new envelopes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:53:15 +02:00
Manos Pitsidianakis
567270e177
melib: use Vec instead of SmallVec for search results
There was no point in using SmallVec with this large a size; it was
32768 bytes, or 32KiB. Let's allocate that to the heap instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:53:15 +02:00
Manos Pitsidianakis
5af6e059b7
meli/accounts: use Arc<str> for account name
Since it gets cloned around a lot.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:53:10 +02:00
Manos Pitsidianakis
630df3083f
CI: Add arm64 runners in job matrices
Add arm64 runs on matrices of jobs that build stuff.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-24 11:49:19 +02:00
Manos Pitsidianakis
b7e215f9c2
melib/utils: fix test_fd_locks() on platforms without OFD support
Not all platforms support open file description locks.

It is part of POSIX-2024 [1] so hopefully this will improve in the future.

[1]: "The Open Group Base Specifications Issue 8 IEEE Std 1003.1-2024" <https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/fcntl.h.html>

Fixes #522 "utils::tests::test_fd_locks fails on macOS"

Resolves: <https://git.meli-email.org/meli/meli/issues/522>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-20 16:45:37 +02:00
Manos Pitsidianakis
c375b48ebf
terminal: fix Synchronized Output response parsed as input
Since 2af5c8b6fd ("terminal: add
QuerySynchronizedOutputSupport WIP") we send a synchronised output
support query [1] to the terminal but we don't parse the response. If the
terminal replies, it gets parsed as user input. And if the user happens
to press a navigation key such as scroll_down which supports a number
modifier, the `2026` part of the mode response which was parsed as user
input makes meli scroll down 2026 lines.

Fixes: #502 ("Initial navigation state in mailbox index view is wrong")

[1]:
<https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036>

Resolves: <https://git.meli-email.org/meli/meli/issues/502>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-20 15:49:49 +02:00
Manos Pitsidianakis
25f0a3f814
conf/terminal: fix serde of ProgressSpinnerSequence
If ProgressSpinnerSequence / progress_spinner_sequence had a value of
just an array of strings, it could not be parsed correctly. Fix that
with a custom serde impl and also add unit tests.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-20 15:49:49 +02:00
Manos Pitsidianakis
2bd8d7ba01
conf/tests.rs: Rename test functions to follow path convention
Rename functions to follow the convention of being prefixed with the
module path before the function name (e.g. `fn
test_module_a_module_b_test_desc() {`)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-20 11:48:08 +02:00
Manos Pitsidianakis
a27a28275b
Bump version to 0.8.8
Finally!

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-19 16:14:21 +02:00
Manos Pitsidianakis
68ec2c53a8
CHANGELOG.md: update to new format
Update commit message entries to new format, by making commit sha
monospace and bold so that they take the same width in the list.

Also, make all commit messages be surrounded by ` to show them in
monospace as well; they might not always include English words but
identifiers and abbreviations instead.

Vim substitutions used:

- %s/- [[]\(........\)[]]/- \[**`\1`**\]/

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-19 14:06:39 +02:00
Manos Pitsidianakis
f036f95eee
scripts: add generate_release_changelog_entry.sh
Add a helper script to generate a new release entry to add to
CHANGELOG.md when it's time to tag a new release.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-19 13:19:04 +02:00
Manos Pitsidianakis
7ff1db143f
manage-mailboxes: add delete option
While there was a delete-mailbox command, there was no option for it in
the manage-mailboxes interface, so add it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-18 16:15:18 +02:00
Manos Pitsidianakis
35fa8e94a6
melib/imap: gracefully retry without DEFLATE on BYE
If server replies with BYE, try one more time without DEFLATE.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-16 11:40:41 +02:00
Manos Pitsidianakis
ecc9b4823e
Small repo cleanups
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-16 11:19:35 +02:00
Manos Pitsidianakis
707a129ea4
Coalesce repeating TUI notification messages
Don't accept new Display Messages if it has the same content as the
latest one in the display message stack.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 19:16:44 +02:00
Manos Pitsidianakis
e7a164de0c
Configure some gpgme stuff under gpgme feature
Some stuff that are only used with gpgme feature were not guarded by
 #[cfg(feature = "gpgme")], so fix that.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 19:16:08 +02:00
Manos Pitsidianakis
3040521695
melib: make notmuch feature depend on maildir feature
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 19:16:08 +02:00
Manos Pitsidianakis
e9ec6761f9
melib: make base64 dep mandatory
base64 is used in melib::backends::utf7 without any sort of conditional
compilation. Make base64 a mandatory dependency.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 19:16:07 +02:00
Manos Pitsidianakis
7b1be139f2
melib: make mbox backend build by default
Allow mbox to be built without the `notify` dependency. Add feature
`mbox-notify` to provide that instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 19:16:07 +02:00
Manos Pitsidianakis
b912aabca2
docs: add examples of file picker usage
Add an example for `fzf` use with the `add-attachment-file-picker < `
command when composing e-mail.

Closes #507

Resolves: <https://git.meli-email.org/meli/meli/issues/507>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-15 12:27:53 +02:00
Manos Pitsidianakis
0e77bd5b4c
melib/email/compose/tests: add multipart mixed attachment test
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-14 20:35:53 +02:00
Manos Pitsidianakis
d5d3457914
melib/email/compose/tests: normalise test fn names
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-14 19:31:06 +02:00
Manos Pitsidianakis
c1901c962d
melib/email/compose: add Content-Type header for utf8 text plain attachments
We should be specifying that the charset is utf-8.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-14 19:30:26 +02:00
Manos Pitsidianakis
d21c686da7
melib/attachments: Make AttachmentBuilder::set_raw generic
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-14 19:28:36 +02:00
Manos Pitsidianakis
bcbcb012ef
melib/email/compose: ensure boundary always prefixed with CRLF
When printing multipart boundaries, e.g.
`--bzz_bzz__bzz__xxxxxxxxxxxxxxxxxxxxxxxxxxx` after ending a sub-part,
ensure there's a CRLF already at the end of the accumulator string
before adding the `--...` boundary tag.

Fixes #503 ("Sending attachments does not work properly")

Resolves: <https://git.meli-email.org/meli/meli/issues/503>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-14 18:29:16 +02:00
Manos Pitsidianakis
cb2dd5def5
listing/threaded: impl missing select functionality
Selecting by searching for was missing for the threaded index style, so
add it.

Related to #513

Resolves: <https://git.meli-email.org/meli/meli/issues/513>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-11 00:55:17 +02:00
Manos Pitsidianakis
c6e9e4245d
listing/threaded: impl missing filter functionality
Somehow, filter (searching) was not implemented for the threaded index
style, so fix it.

Fixes: #513

Resolves: <https://git.meli-email.org/meli/meli/issues/513>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-11 00:41:12 +02:00
Manos Pitsidianakis
320fddad41
melib/gpgme: disable layout tests on non-x86_64 hosts
The generated bindgen layout tests make hard-coded assumptions about
target specific sizes like pointers, let's disable them for now before
we figure out a better long-term solution for gpgme compatibility.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 23:04:11 +02:00
Manos Pitsidianakis
d2559e42e5
imap: return all mailboxes, not just subscribed ones
Meli previously returned only subscribed mailboxes for IMAP. This was
erroneous behavior for two reasons:

- the front end should be able to see all mailboxes because the user is
  allowed to change subscription settings for remote mailboxes from the
  manage-mailboxes interface
- special usage mailboxes, e.g. a Trash folder, would not be visible to
  meli if it was not subscribed

Fixes #508.

Resolves: <https://git.meli-email.org/meli/meli/issues/508>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:40 +02:00
Manos Pitsidianakis
2411481196
manage: parse scroll_{left,right} actions
They were previously silently ignored despite tables (DataColumns)
having support for scrolling on the x axis.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:40 +02:00
Manos Pitsidianakis
789a88b28d
shortcuts: add select_motion equivalent to select_entry
Change select_entry to toggle only one's selection entry (default 'V'),
while select_motion (default 'v') inherits the original functionality of
  performing a select motion when combined with a number and motion verb
  (scroll up/down etc)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:40 +02:00
Manos Pitsidianakis
ad79bf84c2
.gitea/Makefile.lint: attempt cargo-fmt with +nightly
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:40 +02:00
Manos Pitsidianakis
81ace71b35
terminal/embedded: lift error checking earlier
Lift error checks earlier in the function, before forking, allowing any
potential errors to show up properly in the main process.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:39 +02:00
Manos Pitsidianakis
469168959f
melib/gpgme: s/NULL/NUL when referring to NUL byte
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:39 +02:00
Manos Pitsidianakis
50922d97b8
melib/README.md: update and fix feature table
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:39 +02:00
Manos Pitsidianakis
0d088962d5
lints: Address clippy::too_long_first_doc_paragraph
Ugh, annoying. It doesn't take the fact that markdown links do not
appear long when rendered. Anyway, fix them errors.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 15:13:39 +02:00
Manos Pitsidianakis
ee89794248
lints: deny clippy::or_fun_call
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 14:29:40 +02:00
Manos Pitsidianakis
441fda568c
terminal: move TextPresentation trait to melib
Since this is a text-related trait, let's move this to melib's text module
instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 13:40:33 +02:00
Manos Pitsidianakis
f656aff08e
composer: add discard-draft command
Add command to discard draft and close the composer tab immediately.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 13:40:32 +02:00
Manos Pitsidianakis
3a93103520
command: move Composer actions under TabActions
Semantically Composer actions should be tab actions since a composer is
a tab. Move them to a new enum in preparation for the next commit which
adds a new Discard draft action.

No functional change intended.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 13:40:32 +02:00
Manos Pitsidianakis
3558db514a
Move jobs and mailbox management Components together
Move the components for :manage-jobs and :manage-mailboxes into the same
submodule, `manage`. No functional change intended.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 13:40:32 +02:00
Manos Pitsidianakis
dba5b68be8
components: add prelude module
Add prelude module with exports that are commonly necessary for
Component types.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-11-04 13:40:31 +02:00
Manos Pitsidianakis
6c315580b1
compose: fix add-attachment-file-picker
Related to #507

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-24 21:09:18 +03:00
Manos Pitsidianakis
1b708a9990
melib: attempt FromSql from Blob for u64 hash
Attempt to parse u64 hash from Blob column type. This is to fix a
regression from the frontend storing envelope hashes as big endian byte
blobs instead of an i64.

Fixes #504

Resolves: https://git.meli-email.org/meli/meli/issues/504
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-16 21:06:56 +03:00
Manos Pitsidianakis
05f404ba1f
jobs: do not use AtomicU64
AtomicU64 is not portable, unfortunately.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-16 21:03:43 +03:00
Manos Pitsidianakis
ae29494575
remove unused module file
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-14 16:04:38 +03:00
Manos Pitsidianakis
b2200ec3ab
Remove unused smtp tests
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-07 12:35:56 +03:00
Manos Pitsidianakis
c3cac77dee
Update imap-codec dependency to 2.0.0-alpha.4
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-07 12:35:56 +03:00
Manos Pitsidianakis
27486f2908
Accept newer versions of base64 dependency
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-07 12:35:56 +03:00
Manos Pitsidianakis
6cfe4da0c1
Enable rusqlite feature "modern_sqlite" always
Crate fails to build when dynamically linked.
The feature is implicitly enabled when statically linked,
as part of feature bundled-full.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 19:50:58 +03:00
Manos Pitsidianakis
77e7c3df60
Add support for signatures
Add config values to `composing` config section to enable signatures:

   signature_file Path                   (optional) Plain text file with signature that will pre-populate an email draft.  Signatures must be explicitly enabled to be used, otherwise this setting will be ignored.  (None)

   use_signature bool                    Pre-populate email drafts with signature, if any.  meli will lookup the signature value in this order:
                                         1.   The signature_file setting.
                                         2.   ${XDG_CONFIG_DIR}/meli/<account>/signature
                                         3.   ${XDG_CONFIG_DIR}/meli/signature
                                         4.   ${XDG_CONFIG_DIR}/signature
                                         5.   ${HOME}/.signature
                                         6.   No signature otherwise.
                                         (false)

   signature_delimiter String            (optional) Signature delimiter, that is, text that will be prefixed to your signature to separate it from the email body.  (‘\n\n-- \n’)

Closes #498

Resolves: https://git.meli-email.org/meli/meli/issues/498
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 18:50:02 +03:00
Manos Pitsidianakis
b930cb4940
maildir: do not use rename_regex when only updating flags
Concerns #463

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 18:33:12 +03:00
Manos Pitsidianakis
0c590bbc0c
contact-editor: remove empty space
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:50 +03:00
Manos Pitsidianakis
b8e841bbcd
jmap: implement mailbox deletion
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:50 +03:00
Manos Pitsidianakis
ca7eb79284
jmap: Implement deleting email
Finally!

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:50 +03:00
Manos Pitsidianakis
39592ad02c
jmap: implement changing mailbox subscription
Finally!

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:50 +03:00
Manos Pitsidianakis
6d0d968040
jmap: move EmailObject state to Store
State is per account, not per mailbox, so move it to the per-account
store.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:50 +03:00
Manos Pitsidianakis
9865211076
CI: prepend printf commands with @
In Makefiles, prepending a command with @ will not print the command
being executed, only its result. Do this for `printf`s.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:41:49 +03:00
Manos Pitsidianakis
4f927bbe64
nntp: properly return all nntp mailboxes
Previously only mailboxes specified in the configuration were fetched.
Now, all groups returned by `LIST ACTIVE` are examined with the
`is_subscribed` function.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:31:20 +03:00
Manos Pitsidianakis
5915f125c3
backends: use IsSubscribedFn in method signatures
Despite having the IsSubscribedFn struct wrapper for closures that check
if a mailbox is subscribed or not, backend methods were still using
  Box<dyn Fn(... types. Switch to using the wrapper newtype everywhere.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:31:20 +03:00
Manos Pitsidianakis
cd2e4bf3a4
melib/utils: vendor urn crate
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:31:19 +03:00
Manos Pitsidianakis
a6c7621ce3
jscontact: add {created,updated} fields
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-10-05 09:31:17 +03:00
Manos Pitsidianakis
7dee32ae88
contacts: refactor Card to its own module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-24 09:28:53 +03:00
Manos Pitsidianakis
32f7e50fd4
Add version migration support
Add infrastructure for custom logic migration support between versions,
allowing breaking changes in for example configuration to be handled by
the application instead of the user.

The current version will be stored in ${XDG_DATA_HOME}/meli/.version

If at launch the versions do not match, the application will examine
whether there are any migrations in the range
previous_version..=current_version, and if they are applicable to the
user's case, it will ask the user to perform them interactively.

Support for reverting migrations is also added.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-24 08:14:31 +03:00
Manos Pitsidianakis
49dcbc5e58
terminal: Extend Ask default actions, prompts
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-23 22:19:40 +03:00
Manos Pitsidianakis
2069b4da09
errors: impl From<xdg::BaseDirectoriesError>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-23 22:18:43 +03:00
Manos Pitsidianakis
8af003abd0
Rename addressbook stuff to "contacts"
Wasn't a nice name to begin with.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-23 09:44:00 +03:00
Manos Pitsidianakis
593ed22ba1
pgp: perform gpgme's sign+encrypt manually
gpgme's sign and encrypt API doesn't seem to work properly; it only
encrypts for some reason. Do it manually which according to RFC 3156 -
MIME Security with OpenPGP is to sign first then encrypt the whole
thing.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-22 17:18:42 +03:00
Manos Pitsidianakis
e032acfab7
view: pass filtered body to Composer as reply text
When replying to an e-mail, pass the filtered e-mail text to the
composer. This way, processed text like decrypted PGP e-mail is shown in
the reply context of a reply as the user expects.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-22 00:12:59 +03:00
Manos Pitsidianakis
e0cfe8e4d7
Fix compilation for 32-bit architectures
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-21 13:06:02 +03:00
Manos Pitsidianakis
2001b4dd06
Make subscribed_mailboxes conf val optional
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-21 13:06:01 +03:00
Manos Pitsidianakis
1b201bf611
Remove GlobMatch trait, replace usage with Fnmatch
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-21 09:57:15 +03:00
Manos Pitsidianakis
be3b3ef89b
melib/utils: add fnmatch(3) interface
Meant for use with mailbox path globbing.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-21 09:48:44 +03:00
Manos Pitsidianakis
7f0157a966
compose: make dialogs bigger in height
Otherwise they might be hidden in small screens.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-20 08:48:53 +03:00
Manos Pitsidianakis
7e800a8f3f
CI: move manifest_lints.yaml actions to Makefile.manifest-lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-20 08:48:53 +03:00
Manos Pitsidianakis
598a70f9df
CI: move lints.yaml actions to Makefile.lint
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 19:15:30 +03:00
Manos Pitsidianakis
1b3f2732b2
CI: Move build.yaml actions to Makefile.build
So that they can easily be performed locally with

make -f .gitea/Makefile.build

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 19:15:29 +03:00
Manos Pitsidianakis
e6fa7093bf
view/envelope: trim headers values to 3 lines maximum
Prevent large header values (e.g. big list of recipients in To/Cc) take
big part of the view. Allow the full value to be visible when the
`expand_headers` option is toggled on.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:44:19 +03:00
Manos Pitsidianakis
0b6988b7cf
gpgme: add always trust flag to encrypt op
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:41:08 +03:00
Manos Pitsidianakis
23395491db
compose/pgp: add encrypt_for_self flag
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:41:07 +03:00
Manos Pitsidianakis
c82341f3af
File: try trimming filename if ENAMETOOLONG
Commit 8f0e1d6640 added human-readable
identifiers in temp draft files but neglected to handle cases where
those identifiers were too darn long.

Fixes: 8f0e1d664 ("Add human-readable identifiers in temp draft files")
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:41:06 +03:00
Manos Pitsidianakis
b1f24cbe95
view/filters: forward events on child filters
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:41:06 +03:00
Manos Pitsidianakis
a69122f8b0
pgp: use default sign/encrypt keys when no keys are selected
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-19 18:41:06 +03:00
Manos Pitsidianakis
7cfcbb7ab1
Add patch_retrieve module
Allow patches to be retrieved from sources like lore.kernel.org or other
public-inbox instances.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 13:44:50 +03:00
Manos Pitsidianakis
128b959f36
nntp: prepend Newsgroups header if missing on NntpType::submit()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:44:55 +03:00
Manos Pitsidianakis
b27bac7f85
nntp: use DEFLATE when available by default
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:43:52 +03:00
Manos Pitsidianakis
6eeb4571b7
nntp: make all fields public
The backend structs are public, fields should be public too.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:42:59 +03:00
Manos Pitsidianakis
9a9cd03d9d
nntp: add NntpType::article_message_id() method
Add a method to retrieve an article by message id.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:40:41 +03:00
Manos Pitsidianakis
592ce15903
mbox: use Uuid::nil() as default envelope from
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:39:15 +03:00
Manos Pitsidianakis
fc3308e428
melib: Add Mail::as_mbox() method
While at it, cleanup Debug impl to prevent printing huge byte arrays.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:38:18 +03:00
Manos Pitsidianakis
b33433e457
Don't create backends as Box<dyn MailBackend>, but as Box<Self>
It's unnecessary and makes it difficult for an API consumer to turn a
backend back to its original type.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-17 10:35:55 +03:00
Manos Pitsidianakis
5f120309f9
nntp: add select_group_by_name() method
Add NntpConnection::select_group_by_name() method.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-14 12:52:52 +03:00
Manos Pitsidianakis
6bc0caf4e0
melib: remove redundant get_path_hash macro
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-14 12:50:12 +03:00
Manos Pitsidianakis
d4636bcc70
nntp: interpret IMPLEMENTATION cap as metadata
When returning MailBackendCapabilities for an Nntp backend, interpret
the IMPLEMENTATION capability as server metadata.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-14 12:38:48 +03:00
Manos Pitsidianakis
aaea3a5ab4
nntp: add timeout conf flag
To match IMAP and JMAP, add a configuration flag `timeout` to use for
server connections.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-14 10:52:47 +03:00
Manos Pitsidianakis
b048c95a86
BUILD.md: add instructions for Android build
With termux's unofficial Rust toolchain package.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-14 10:38:22 +03:00
Manos Pitsidianakis
f78884ce02
melib/nntp: fix an ancient FIXME
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-13 00:49:17 +03:00
Manos Pitsidianakis
571ae390b8
pager.rs: don't set self dirty after filter selector
After generating a filter selector global dialog, there's no need to
set self as dirty.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-12 21:47:07 +03:00
Manos Pitsidianakis
2ddd28ee85
main.rs: always send a JobFinished event to all components
JobFinished events were sent from account job handlers, but that means
the events would never be generated when using meli without accounts
e.g. with the `view` subcommand.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-12 21:45:45 +03:00
Manos Pitsidianakis
7dbee81dad
view: fix nested filter jobs never being completed
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-12 21:44:42 +03:00
Manos Pitsidianakis
46b2c3b1f7
Add listing.thread_layout config flag
Allows the user to set the default thread layout and takes the values
"auto" | "vertical" | "horizontal". The layout can always be changed at
runtime with the `listing.toggle_layout` shortcut.

Closes #484

Resolves: <https://git.meli-email.org/meli/meli/issues/484>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-12 15:15:52 +03:00
Manos Pitsidianakis
d0c81749ee
conf::data_types: minor style and error msg fixups
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-12 15:15:51 +03:00
Manos Pitsidianakis
ba3ad8ed18
listing: always show mail_view_divider
`mail_view_divider` was only drawn in the path where the sidebar was
visible.

Concerns #483

Fixes: 719e2eb271 ("listing: add customizable view divider like sidebar's")
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-10 16:47:01 +03:00
Manos Pitsidianakis
719e2eb271
listing: add customizable view divider like sidebar's
Closes #483

Resolves: <https://git.meli-email.org/meli/meli/issues/483>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-09 16:31:35 +03:00
Manos Pitsidianakis
b88dc4412b
Comment out svgfeature; no need to ship it
Svg feature was meant for taking SVG screenshots for the readme/website.
There's no real need to ship it, considering a lot of distros enable all
features by default.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
e9a72072bf
Remove unused/obsolete plugins code and mentions
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
c051190114
Update debian/meli.{docs,examples} and Cargo exclude
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
d20a9d0afa
Fix new clippy lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
601e37117c
Add vCard exports
Add shortcut to export contact under cursor in the contact list as a
.vcf file (default: `E`).

Also add an export button in the contact editor form.

Closes #424

Resolves: <https://git.meli-email.org/meli/meli/issues/424>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
dc9e91df1f
contacts/editor: Use FormButtonAction in form
Instead of bool.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:03 +03:00
Manos Pitsidianakis
6d520605ff
Vendor vobject crate
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 11:30:02 +03:00
Manos Pitsidianakis
0c0f821000
Add a "move to Trash" shortcut
Add send_to_trash shortcut (default value: `D`) to send an entry or
selected entries to the Trash folder.

Closes #389

Resolves: <https://git.meli-email.org/meli/meli/issues/389>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:39 +03:00
Manos Pitsidianakis
8f0e1d6640
Add human-readable identifiers in temp draft files
Draft files that are created in temp directories to be opened and edited
by the user's editor get UUID filenames with the `.eml` extension. Give
them filenames with the draft subject, recipient and date to make it
easier to discern a file's identity on the filesystem.

Closes #466

Resolves: <https://git.meli-email.org/meli/meli/issues/466>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:39 +03:00
Manos Pitsidianakis
e9b87b2e40
melib/maildr: add rename_regex config option
Add optional rename_regex configuration option to allow stripping
patterns from pathnames when renaming them. This is useful when other
programs depend on specific substrings being unique like mbsync which
erroneously assumes UIDs are unique instead of UID+UIDVALIDITY+mailbox
name like the IMAP standard specifies.

Closes #463

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:39 +03:00
Manos Pitsidianakis
4c44c440f6
melib: #[ignore] shellexpand tests
These tests fail sometimes, #[ignore] them until the bug is found.

Closes #442

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:39 +03:00
Manos Pitsidianakis
5c4faea539
Add transpose shortcut and tests for text field
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:39 +03:00
Manos Pitsidianakis
2af5c8b6fd
terminal: add QuerySynchronizedOutputSupport WIP
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:37 +03:00
Manos Pitsidianakis
00236b86f6
docs: add meli.conf.examples(5) WIP
Closes #461

Resolves: <https://git.meli-email.org/meli/meli/issues/461>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:37 +03:00
Manos Pitsidianakis
90974e7c0d
imap: cache miss if row env hash != row hash
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:37 +03:00
Manos Pitsidianakis
4a26cfa106
logging: disable tracing from output
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:37 +03:00
Manos Pitsidianakis
cbafdcf734
terminal: color report WIP
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-08 09:21:31 +03:00
Manos Pitsidianakis
7c056e4bdb
Retry loading mailbox on recoverable error
If a mailbox status is an error and it is recoverable (e.g. a connection
time-out) allow retrying.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 12:17:46 +03:00
Manos Pitsidianakis
8205c7f51a
melib: add JsContact module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:07 +03:00
Manos Pitsidianakis
ac1349b850
command: alias pwd to cwd
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
32acc3474f
view: show signature verification properly
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
b950fceab4
melib: Use IndexMap in VCard
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
65b32e7719
subcommands: Fix wrong help info in imap-shell prompt
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
b55edd4727
debian: update meli.docs and add meli.manpages
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
a44486d904
imap: fix minor clippy lint
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:06 +03:00
Manos Pitsidianakis
14d74f3689
Update smol dependency from "1" to "2"
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-03 10:14:05 +03:00
Manos Pitsidianakis
67b88d24fc
Update polling dependency from "2.8" to "3"
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-02 22:42:04 +03:00
Manos Pitsidianakis
57b45a9c4a
docs/historical-manpages: add DEP5 copyright file
Add debian copyright file for historical manpages. Copyright was
mentioned in the manpage comments, but wasn't otherwise visible.

While at it, make files ending up at .gz actually gzip'ed.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-02 22:42:04 +03:00
Manos Pitsidianakis
1232e16ad9
scripts/make_html_manual_page.py: don't prettify
Prettify in beautifulsoup4 messes up with newlines, which alters the
html content.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-02 22:42:04 +03:00
Manos Pitsidianakis
8091583221
mailto: rewrite parsing
Rewrite parsing to fix error on escaped ampersands as html entities in
mailto value.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-02 21:07:06 +03:00
Manos Pitsidianakis
a55f65e131
meli.conf.5: Fix wrong default value type in default_header_values
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-09-02 11:52:51 +03:00
Manos Pitsidianakis
335cca88cb
listing: fix highlight_self flag off by one error
highlight_self flag was not shown if row had the maximum amount of
visible flags in the current page, because the width of the highlight_self
flag was not taken into account for the maximum column width.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 22:26:57 +03:00
Manos Pitsidianakis
4e967280e1
nntp: don't needlessly select group before ARTICLE
GROUP was sent before ARTICLE every time even if the group was already
selected. Use Connection's `select_group` method that makes sure the
group is not re-selected needlessly.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:13 +03:00
Manos Pitsidianakis
2d320688ce
mail/listing: pre-lookup conf values
Lookup conf values out of the for loop.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:13 +03:00
Manos Pitsidianakis
1779ad5d3a
imap: interpret empty server response as BYE
This should trigger a reconnect to the IMAP server.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:12 +03:00
Manos Pitsidianakis
1e11c29c88
imap: resync cache first when fetching a mailbox
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:12 +03:00
Manos Pitsidianakis
e48fcc3367
imap/protocol_parser: also populate other_headers
Also populate other_headers when creating a new Envelope

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:11 +03:00
Manos Pitsidianakis
151fcebe5b
imap: use BTreeMap for message sequence number store
Populating the message sequence number runtime store is not performed in
order, so inserting UIDs in a vec can fail when we want to insert a
number higher than the current maximum. This could lead to panics when
inserting to a vec at an index above its length.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:11 +03:00
Manos Pitsidianakis
4d4e189cb9
imap: code style fixups
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:11 +03:00
Manos Pitsidianakis
b798ca4a95
imap: return cached response in {select,examine}_mailbox()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:10 +03:00
Manos Pitsidianakis
77da86eb0f
CI: Update cargo-derivefmt version
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:10 +03:00
Manos Pitsidianakis
41e1fdd554
Fix cargo-derivefmt lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:10 +03:00
Manos Pitsidianakis
86e25bc017
sqlite: fix database reset sequence
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:09 +03:00
Manos Pitsidianakis
604ae11128
Impl From<&[u8]> for u64-based hash newtypes
`<_>::from_bytes(bytes: &[u8]) -> Self` already exists, so add
`From<&[u8]>` also.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:09 +03:00
Manos Pitsidianakis
a7c73fc8cf
gpgme: refactor Rust interface, add tests
- Refactor gpgme wrapper interface to use more robust reference counting
  for the gpgme context object
- Add SAFETY comments to unsafe {} blocks
- Add tests

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 14:27:09 +03:00
Manos Pitsidianakis
87d2cec9d9
Add sealed_test dependency
Some tests needs a standalone environment because they mess with
variables that other tests need too. sealed_test runs the tests in a
forked process so that there are no issues with that.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:36 +03:00
Manos Pitsidianakis
d6197e8b24
listing: clear count modifier on Home/End
Home/End count as page/scroll movements, but they'd not clear the
modifier buffer since they weren't using it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:36 +03:00
Manos Pitsidianakis
dbbb1529e4
Add missing ComponentUnrealize handlers
Dialog widgets require their parent components to handle their
ComponentUnrealize events, otherwise they'd not be removed if user
selects 'Cancel'.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:35 +03:00
Manos Pitsidianakis
4707ec9f2a
text/line_break: fix ReflowState::{No,All} break
It could split text on non-char boundaries. Check before return.

Closes #476

Resolves: https://git.meli-email.org/meli/meli/issues/476
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:35 +03:00
Manos Pitsidianakis
32e3be8b10
sqlite3: add optional directory field in DatabaseDescription
Databases described by `DatabaseDescription` are created in XDG Data
directories by default. Add an optional explicit directory field so that
tests and other API consumers can override that location.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:35 +03:00
Manos Pitsidianakis
26d33ce523
address: add separator argument to display_slice()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:35 +03:00
Manos Pitsidianakis
394236ba8a
email/address: Refactor References struct
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-28 10:36:34 +03:00
Manos Pitsidianakis
a4f344b396
Use create_new to avoid overwriting files
Use File::options()..create_new() to avoid overwriting files when saving
attachments or exporting stuff.

File::create_new was added in 1.77 which is after our current MSRV, so
use OpenOptions instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
11798be804
Replace Envelope::message_id_display() with Display impls
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
6fbf569fe0
search: add Message-ID, and other header search support
Add support for searching by Message-ID, In-Reply-To, References, or any
header with the following keywords:

- "message-id:term", "msg-id:term"
- "in-reply-to:term"
- "references:term"
- "header:title,value"

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
191725b5c2
Fix some borrow checker error/warnings from upcoming 2024 edition
Mostly unsafe_op_in_unsafe_fn lint, but also std::env functions becoming
unsafe.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
2bb9b20d95
mail/view: do not highlight reply subjects in thread
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
28f45805d7
mail/view: try cancel env fetch on Drop
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
60833ee51d
accounts: make mailbox available as soon as possible
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:30 +03:00
Manos Pitsidianakis
53b0d035e4
accounts: cancel any previous mailbox fetches
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
f06a9072d6
jmap: fetch mailbox with receivedAt descending sort
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
9c1b442452
jobs: make cancel flag an AtomicBool
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
4bbf446bc1
utils: add unix file locks module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
1cfb0b1538
Update nix dependency to 0.29.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
72dea6f3b2
Manpage fixes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-23 14:48:29 +03:00
Manos Pitsidianakis
f3ad824df9
meli: use itoa to format offset indices in listings
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 15:59:24 +03:00
Manos Pitsidianakis
aed7a60fb9
samples: add ibm-modern theme
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 14:18:13 +03:00
Manos Pitsidianakis
0ee7fc4d95
Print clickable path links with subcommands
Print clickable path links when printings paths via subcommands like
print-log-path.

Part of #445 ("Add OSC8 terminal hyperlink support")

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:49 +03:00
Manos Pitsidianakis
6be5fd2610
themes: add inheritance, and use themes when initializing grids
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:49 +03:00
Manos Pitsidianakis
036586a2f7
Update serde dependency to 1.0.205
This release solves a clippy lint warning for code that will get
introduced in the next commit.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:49 +03:00
Manos Pitsidianakis
bf3a4c5dc1
error: add ErrorChainDisplay struct for better output
Add an ErrorChainDisplay struct that borrows an error and prints the
list of chained errors one by one, showing relationships and metadata
for each error.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
4b959f5c4f
Remove pcre feature/dependency
Crate regex is used by default in builds, so there's no need for an
optional libpcre2 dependency

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
978cefbb50
Replace Escape ascii char with hex literal
rustfmt was acting weird with it

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
a214a35c02
conf: refactor into submodules
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
8d45ecc15d
melib/error: add related_path field
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
de72bc6ac7
melib/error.rs: move network stuff to submodule
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:48 +03:00
Manos Pitsidianakis
0bed37b5a7
melib: use IndexMap in conf fields
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-11 13:53:47 +03:00
Manos Pitsidianakis
2084ce9375
Fix invalid cfg feature combinations for macos
NOTMUCH_ERROR_DETAILS was defined twice for macos from "unix"
target_family and target_os = "macos".

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-10 12:50:44 +03:00
Manos Pitsidianakis
35f12b1551
embedded: prevent double-close of pty fd
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-06 18:15:11 +03:00
Manos Pitsidianakis
eda6620cb4
jmap: detect supported Auth schemes on connect
When 401 is returned by an HTTP server, the WWW-Authenticate header must
be present. The values represent the supported Authentication schemes of
the server.

This commit detects them and reports the appropriate error to the user.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-06 16:45:51 +03:00
Manos Pitsidianakis
7419b465ea
CI: unpin rust version after updating time dependency
This reverts commit 8a74920d (CI: pin rust version to 1.79.0, 2024-07-28).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-04 19:33:41 +03:00
Manos Pitsidianakis
6ee148c041
Fix 1.80.0 clippy lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-04 19:33:41 +03:00
Manos Pitsidianakis
6b05279a09
Update time dep to fix 1.80.0 breakage
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-04 17:00:05 +03:00
Manos Pitsidianakis
4684b6016b
CI: remove env vars from action names
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:43 +03:00
Manos Pitsidianakis
94f345d731
Implement mailbox renaming command
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:43 +03:00
Manos Pitsidianakis
15d24ab0e3
meli/jobs: refactor spawn_{blocking,specialized} to spawn()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
f7ec6d6bc5
melib/jmap: implement mailbox rename
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
8481294147
melib/jmap: do not serialize server-set fields in Set create
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
e6877e89c2
melib/jmap: refactor some parser imports
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
2b3828d8d2
Update futures dependency to 0.3.30
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
7be8912c14
Cargo.tomls: make formatting more consistent
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
9e9c04a3f6
Update indexmap dep to 2.3.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:42 +03:00
Manos Pitsidianakis
a8dad31776
melib/imap: renamed cache module to sync
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:41 +03:00
Manos Pitsidianakis
6513c18810
melib/imap: on sync only update exists/unseen if loaded
When resyncing with resync_basic and resync_condstore, we would update
the unseen and exists counts for the mailbox without inspecting if the
mailbox was loaded or not. Since a previous commit reset total not yet
seen to 0 when inserting new entries to mail totals, this would lead in
unloaded mailboxes go from having their original unseen counts to having
only the new envelopes in the total.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:41 +03:00
Manos Pitsidianakis
84cfa358de
conf: remove need for global send_mail setting
This was a bad UX artifact from the very first meli versions. There's no
need to keep it around.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 16:03:41 +03:00
Manos Pitsidianakis
56b1bf28eb
meli/accounts: batch process refresh events
Process refresh events for a mailbox in a batch.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 15:27:23 +03:00
Manos Pitsidianakis
14f2d91193
melib/backends: change RefreshEvent field decl order
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 15:27:23 +03:00
Manos Pitsidianakis
6906585942
accounts: split mailbox to enum out of JobRequest
Split mailbox related jobs out of JobRequest enum to its own enum since
JobRequest is getting too big to be readable.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 15:27:23 +03:00
Manos Pitsidianakis
3216324ccc
melib/mbox: impl FromStr for MboxFormat
Add a FromStr implementation so that we can parse a string into a format
value from other places in the code.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 15:27:23 +03:00
Manos Pitsidianakis
7020cd6698
meli: derive PartialEq/Eq for some types
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-08-03 15:27:22 +03:00
Manos Pitsidianakis
33836a3263
melib/error: add WrapResultIntoError helper trait
Add a new WrapResultIntoError trait with method wrap_err() so we can
wrap a result and set its error as a source for a new error and
description, this allows for more descriptive error messages like:

Before:

let mbox_format = parse(some_string)?; <- error msg is "invalid mbox
format value, expected blah blah"

After:

let mbox_format = parse(some_string).wrap_err(|| "Could parse
configuration of account {}", account_name)?; <- error msg is "Could not
parse configuration of account myaccount, Caused by: invalid mbox format
value, expected blah blah"

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:22:13 +03:00
Manos Pitsidianakis
374ea8bacb
accounts: extract tests to tests.rs file
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:22:13 +03:00
Manos Pitsidianakis
201081b6d4
meli/command: move tests to tests.rs
Move all command tests to a test submodule file.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:22:13 +03:00
Manos Pitsidianakis
20d7329263
melib: replace async-stream dep with async-fn-stream
Replace proc-macro stream impl with a non-macro one for better
compilation times

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:22:13 +03:00
Manos Pitsidianakis
8e300c4661
melib/jmap: call req text(). asap
Call text() on a RequestBody as soon as it is returned, to avoid
exceeding timeout values, according to isahc documentation.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:22:13 +03:00
Manos Pitsidianakis
f3d59ebf64
accounts: add force: bool arg to load()
Sometimes watch futures from backends send refresh events for mailboxes
that have not been fetched yet. That'd trigger fetching them in the
background, which is bad when the mailboxes are big and not wanted by
the user in the first place.

This commit adds a force parameter that specifies whether the loading
must be forced (e.g. when wanting to display the mailbox) or not.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:21:08 +03:00
Manos Pitsidianakis
a83b4176b0
meli.1: small fixes
- Use .Oo/.Oc for inline Optional arguments
- Replace "thread" selection with "entry"
- Spell out alternative arguments in set seen | unseen

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-31 08:20:39 +03:00
Manos Pitsidianakis
6ae2cf0b6b
Bump version to 0.8.7
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 17:23:38 +03:00
Manos Pitsidianakis
e329097d0b
melib/jmap: fix EmailBodyPart schema mismatch
EmailBodyPart required partId and blobId to be non-null, which is
invalid. Make them Option<_>s instead.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 17:23:38 +03:00
Manos Pitsidianakis
f2e9cac38e
melib/jmap: use suggested minimum for maxObjectsInGet
Instead of using the actual maxObjectsInGet batch size when fetching
email, use min(maxObjectsInGet, 500). This makes things not time-out
this easily when the maxObjectsInGet is large (e.g. 4096)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:51:25 +03:00
Manos Pitsidianakis
41d07fbcef
melib/jmap: newState in EmailImportResponse cannot be null
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:51:20 +03:00
Manos Pitsidianakis
0b113cdbe1
Makefile: use MELI_FEATURES in all cargo invocations
Use env var MELI_FEATURES in all cargo invocations if set, otherwise use
--all-features

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:50:32 +03:00
Manos Pitsidianakis
52c75e92b5
melib/jmap/email.rs: use HeaderName constants
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:08:22 +03:00
Manos Pitsidianakis
8b568f6e3b
melib/jmap: add if_in_state argument in Set::new()
JMAP code should be using if_in_state wherever possible, to detect
mismatch between current server state and current client state.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:06:45 +03:00
Manos Pitsidianakis
46df4b573b
melib/attachments: remove unused function stub
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:05:39 +03:00
Manos Pitsidianakis
85a55ed633
melib/backends: add some missing ErrorKinds to errors
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 12:04:10 +03:00
Manos Pitsidianakis
824de287b4
melib/jmap: fix make_address! use
make_address! macro was called with a .take() expression, which was
substituted in the macro more than once hence take() would be called
more than once.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 11:00:38 +03:00
Manos Pitsidianakis
197132cca0
melib/imap: support fetching with BODY[] for buggy servers
It appears icloud IMAP servers do not support fetching RFC822 items with
the FETCH command. They reply to the fetch successfully but with the
item missing. To support this clearly non-standard and buggy behavior,
retry one more time by fetching with BODY[] instead if the RFC822 is
missing.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-30 10:54:54 +03:00
Manos Pitsidianakis
8a74920dc7
CI: pin rust version to 1.79.0
Stable release 1.80.0 has a regression causing compilation errors:

  error[E0282]: type annotations needed for `Box<_>`

Pin stable version to 1.79.0, since we don't need anything later than
our MSRV anyway.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-28 20:25:48 +03:00
Manos Pitsidianakis
97af00cd83
melib/jmap: respect timeout value from user configuration
While the timeout value was parsed, it was only used for mutex lock
timeouts and not the http connection.

This commit sets the isahc HTTP connections timeouts using the value, if
provided.

Fixes #422

Resolves: https://git.meli-email.org/meli/meli/issues/422
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-24 17:29:52 +03:00
Manos Pitsidianakis
15eeac5191
melib/jmap: enable dns_cache, tcp_keepalive & tcp_nodelay
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-24 17:29:52 +03:00
Manos Pitsidianakis
27ac3061e1
mail/status.rs: fix tag support not being printed
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-19 02:37:43 +03:00
Manos Pitsidianakis
afccebf331
melib/imap: add AUTH=PLAIN support
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-18 11:01:57 +03:00
Manos Pitsidianakis
f5f1e06878
melib/imap: add UIDPLUS support
Add UIDPLUS extension support (Only UID EXPUNGE)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-18 11:00:59 +03:00
Manos Pitsidianakis
06437e607c
melib/LazyCountSet: set not_yet_seen to 0 when inserting existing
Not yet seen behavior is unreliable and leads to false unread counts.
Set it to 0 instead when inserting an existing count.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 18:26:03 +03:00
Manos Pitsidianakis
a13bf13f24
meli/terminal/embedded: add stub Undercurl support
Add consumption of undercurl escape codes without parsing colors, as
there is no support for that in Cell yet.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 17:19:27 +03:00
Manos Pitsidianakis
6379fbe8f4
meli/terminal: add support for Undercurl attribute
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 17:19:27 +03:00
Manos Pitsidianakis
1e2e3da02f
meli/terminal/embedded: Treat color input ; ; as ; 0 ;
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 16:54:15 +03:00
Manos Pitsidianakis
8552e499e8
meli/terminal/embedded: replace std::mem::{replace,take}
Non-functional change.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 16:54:15 +03:00
Manos Pitsidianakis
96cc02a000
melib/imap/error: do not use ErrorKind::Configuration
Do not use ErrorKind::Configuration for ValidationError

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 16:54:15 +03:00
Manos Pitsidianakis
91fdef9820
melib/imap/cache: return NotFound on cache miss
Set error kind to NotFound instead of Bug, since it is possible a
mailbox state is not cached. Also add convenience function
ignore_not_found() that converts NotFound Err Results to Ok(()).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-17 16:54:15 +03:00
Manos Pitsidianakis
b7da1d0f99
CI: check all targets in cargo-msrv verify test
cargo-msrv verify was verifying only library/binary targets, and missed
the test targets. Specify the check command to use so that all targets
are checked.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-15 13:48:49 +03:00
Manos Pitsidianakis
9f783d9a07
meli/Cargo.toml: pin assert_cmd ver to 2.0.13
Version 2.0.14 onwards raises MSRV which is newer than the one we
support.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-15 13:46:21 +03:00
Manos Pitsidianakis
12695a00da
meli/tests: fix MSRV breakage
Commit 814af0e94d broke compilation with MSRV, it was not caught by CI
tests because they do not check the test targets for MSRV verification.

Fixes: 814af0e94d ("meli/args: add --gzipped flag to man subcommand")
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-15 13:43:38 +03:00
Manos Pitsidianakis
86f9b213bf
melib/jmap: add timeout conf field in validate()
`timeout` while valid was not checked in the validate() function, so it
could not be used: the validation would reject it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 22:11:30 +03:00
Manos Pitsidianakis
073aef8671
Fix lints/errors when compiling specific feature combos
- Building with sqlite3 non-bundled may result in compile-time error if
  the system version is not new enough; the bindgen feature must be
  enabled to create ffi bindings from the system version.
- Some unused code warnings show up if some features are disabled,
  refactor code to make them go away.
- Update libloading to 0.8 to prevent dual dependencies in
  Cargo.lock (rusqlite's bundled feature requires it)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 18:30:35 +03:00
Manos Pitsidianakis
475860c946
meli/subcommands: accept - for stdio in {create,test}_config
Accept "-" operand in create_config and test_config subcommands as
stdout and stdin respectively.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 17:09:19 +03:00
Manos Pitsidianakis
814af0e94d
meli/args: add --gzipped flag to man subcommand
Add --gzipped flag to print a manpage without decompressing it, for
piping the output to a file. Because why not.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 17:09:19 +03:00
Manos Pitsidianakis
738f7c4695
meli/main.rs: execute Opt subcommand in Opt::execute()
Move execution of opt.subcommand, if it is given, in a method of the Opt
struct. The main() function is already too long given that it sets up
and handles the event loop, so this reduces the complexity a bit.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 13:50:19 +03:00
Manos Pitsidianakis
2dc1721a49
meli/main.rs: move signal handling stuff to submodule
Move signal handler notify setup function to its own submodule.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-13 13:48:06 +03:00
Manos Pitsidianakis
1e50911c55
melib/imap: add utils module to protocol_parser
Add utils module with parser helpers for syntax items that can be NIL

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-12 16:04:32 +03:00
Manos Pitsidianakis
0dc2462358
mail/listing: fix one by off error on menu unread count
Last digit of unread counts when showing scroll bar was overwritten by
scroll bar. If scroll bar is visible, shift the unread count one column
to the left.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-12 14:59:46 +03:00
Manos Pitsidianakis
a8e82a302b
meli/docs/meli.conf.5: Add missing entries from JMAP
Configuration entries `use_token` and `timeout` were not included in the
JMAP section so add them.

Related to #422

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-11 09:08:36 +03:00
Manos Pitsidianakis
2429f17b44
melib/jmap: on invalid conf value, print what value is expected
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-11 09:06:34 +03:00
Manos Pitsidianakis
3ebf5510ea
meli/state.rs: pass entire screen area when drawing overlay
Let the widget decide how much area it needs instead of providing a
limited subsection. On small screens it can lead to not enough space.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-10 18:30:55 +03:00
Manos Pitsidianakis
a9122c6e34
meli/terminal/screen.rs: draw with x range argument
Instead of providing the horizontal range to draw as x_start and x_end
values, provide a range instead. This is to ensure that when you want to
draw from a bounds/row iterator, which has a col() method that returns
std::ops::Range<usize>, you don't end up drawing from col().start up to
including col().end like I foolishly did.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-10 18:23:25 +03:00
Manos Pitsidianakis
a85b3a089f
meli/accounts: allow default_mailbox to be any mailbox
.. And not just the ones defined in the configuration. Previously, it'd
only work if the mailbox was defined in the configuration file.

Also, make it so that the default mailbox is selected when meli is first
invoked, not just when changing to another account later.

Fixes #350

Resolves: https://git.meli-email.org/meli/meli/issues/350
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-09 13:40:33 +03:00
Manos Pitsidianakis
a330ff96e9
meli/accounts: retry on DNS failure
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-09 11:02:36 +03:00
Manos Pitsidianakis
d1499242b2
melib/error: add From<Infallible> impl
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-09 11:01:54 +03:00
Manos Pitsidianakis
386208664b
melib/error: detect DNS lookup std::io::Error
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-09 11:01:17 +03:00
Manos Pitsidianakis
100fa8b3d1
melib/utils: fix edge case in ShellExpandTrait
If there is no "/" prefix after stripping the argument prefix, the
result was reverted.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-09 09:37:55 +03:00
Manos Pitsidianakis
608301dc3d
mail/view/envelope: Expand save-to paths asap
Paths entered via command were not expanded as soon as the text was
converted into a PathBuf, but when the save-to-file function was called.
This resulted in tilde expansion not happening until then, which because
of buggy behavior the previous commit fixed (expanding the tilde in the
middle of a path and pushing it to an existing path overwrites its
value, because expanding the tilde usually results in an absolute path),
resulted in the correct behavior. The invalid path was still visible on
the success message.

Instead, expand the path right away and then do modifications like
setting filename etc.

Fixes: #431
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-07 22:38:47 +03:00
Manos Pitsidianakis
39e903b1d3
melib/utils: fix issues with ShellExpandTrait
ShellExpandTrait was not expanding paths properly: tilde was expanded
even if it wasn't the first byte in the sequence. Since $HOME tends to
be an absolute path, the entire value of the path accumulator up till
that point was overwritten, meaning a /path/to/~/some/file would get
expanded to ${HOME}/some/file.

ShellExpandTrait was also not completing paths properly, especially
between the generic impl and the linux specialized one. This commit adds
test to make sure their behavior is synced and changes the result type
to an enum to make the result more descriptive.

Concerns #431.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-07-07 22:38:47 +03:00
Manos Pitsidianakis
9daf943758
meli/tests: add test_cli_subcommands.rs
Add test to catch any failure of CLI subcommand.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-30 16:45:44 +03:00
Manos Pitsidianakis
8dc4465c58
Fix toml value ser after update of toml dependency
Update in v0.8.5 required changes to serialization code;
toml::to_string() now serializes TOML documents, not values.

This commit replaces all occurances with toml::Value serializations.

Fixes: 8fff7401 ("Update yanked zerocopy dependency")
Closes: #429
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-29 21:15:04 +03:00
Manos Pitsidianakis
5110813e87
melib/maildir: refactor MaildirOp and watch()
Refactor to remove unwraps and add None checks with returning errors
where appropriate.

Hopefully addresses #426

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-29 14:58:07 +03:00
Manos Pitsidianakis
a82d1e1ebe
mail/listing.rs: fix RowsState::rename_env stale data
RowsState::rename_env was not updating all EnvelopeHash related fields
when a receiving an EnvelopeRename event, which can cause panics if the
listing tries to index it.

Concerns #426

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-29 12:30:33 +03:00
Herby Gillot
dfc2bb4311 readme: add link to MacPorts page for meli
Signed-off-by: Herby Gillot <herby.gillot@gmail.com>
2024-06-29 02:22:44 -04:00
Manos Pitsidianakis
84d93d6555
melib/imap: add support for ID extension (opt-in)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:11 +03:00
Manos Pitsidianakis
af6838c20c
melib: add metadata field to MailBackendCapabilities
Add field for arbitrary metadata as a serde_json::Value.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:11 +03:00
Manos Pitsidianakis
d40ee6928f
melib/imap: extract tests mod from protocol_parser
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:11 +03:00
Manos Pitsidianakis
7c47f70217
melib/sieve: extract test and parser modules to files
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:11 +03:00
Manos Pitsidianakis
ebc1fa3b8a
melib/sieve: move module to self dir
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:11 +03:00
Manos Pitsidianakis
fd76df7889
meli/state: use MELI_CONFIG env var in mock tests
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
5b6c1aa88c
mail/status.rs: don't show all background jobs
Information can be retrieved from :manage-jobs tab, so it's redundant to
repeat it here.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
e96e9789db
melib/imap: don't discard pre-auth capabilities
The library user can retrieve capabilities from a mail backend, and they
might want to see pre-auth capabilities as well.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
e3c1656e05
melib/imap: fix LOGINDISABLED support
LOGINDISABLED disables the LOGIN command, not authenticating in general;
doh! 🤦🤦🤦🤦

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
084a222a51
mail/status.rs: remove subscribed mailboxes list
Info is also available in :manage-mailboxes tab, so it's redundant.
Remove it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
122a2a4d76
accounts: drain event_queue when mailbox made available
RefreshEvents where put into event_queue field, but were never processed
again. Process them when Mailbox becomes available.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:10 +03:00
Manos Pitsidianakis
97aa6a8e6c
meli/docs: replace obsolete .Tn macro with .Em
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
9fb5bc41b4
melib/imap: impl AUTH=ANONYMOUS (RFC4505)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
1cce8c1162
melib/imap: accept invalid "+" CRLF cont req
Some servers erroneously send a "+" CRLF sequence instead of "+" SP
CRLF.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
a9e9d952d5
melib/imap: change termination_string arg to Option
Change ImapConnection::read_lines function argument termination_string
from &[u8] to Option<&[u8]>.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
eb27773b47
meli: add pager.named_filters setting
This new setting defines shell commands with names that can be applied
at any time on a text attachment by using the `filter` command without
any arguments.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
3251e7bd61
meli/config_macros.rs: scrub skip_serializing_if from attributes
If a config field has a skip_serializing_if value in the serde
attribute, remove it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:09 +03:00
Manos Pitsidianakis
6e1fea8059
melib/jmap: show suggestions on Unauthorized error
Show suggestions for solving Unauthorized connection errors.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
15f3a3fba6
meli/view: retry fetch envelope only if err.is_recoverable()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
f9a3b33397
melib/imap/operations: return NotFound on empty FETCH
If UID FETCH returns an OK response but without untagged fetch
responses, it means the UID was not found. Return a NotFound error in
this case and a ProtocolError in other error cases.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
dd525bd940
meli/accounts: use Error::is_recoverable
Replace IsOnline::is_recoverable(err: &Error) with
new method Error::is_recoverable()

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
8c880dc747
melib/error: add {Error,ErrorKind}::is_recoverable()
Add methods to guess whether the error is transient and can be recovered
from.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
7200589a9e
melib/error: add ErrorKind::NotFound
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:08 +03:00
Manos Pitsidianakis
430cbdfd42
scripts/make_html_manual_page.py: fix python errors
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:07 +03:00
Manos Pitsidianakis
fe604bf0ea
Update "openssl" dependency to 0.10.64
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:07 +03:00
Manos Pitsidianakis
9fcb0a0451
Add cargo-deny configuration file deny.toml
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:07 +03:00
Manos Pitsidianakis
bbe2cffafe
Add rust-bindgen's friends.sh to scripts/
I run something similar to get each releases' contributor names, and
rust-bindgen happens to have this in a script, so let's import it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:07 +03:00
Manos Pitsidianakis
6da4e2eca6
Replace stringify! in Debug impls with type checked macro
Add identify! macro which expands to stringify! but also const type
checks the value to prevent typos/wrong values.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:07 +03:00
Manos Pitsidianakis
e107d613a0
melib/backends: add prelude module for import cleanup
Add a prelude module under melib::backends that exports all types needed
to interact with a mail backend. This reduces the number of imports in
every related file.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:15:05 +03:00
Manos Pitsidianakis
c99633e141
Update futures dependency 0.3.28 -> 0.3.30
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:14:05 +03:00
Manos Pitsidianakis
9ab404c57a
view/filters.rs: add pgp signed attachment support
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:14:05 +03:00
Manos Pitsidianakis
c4f7b77a39
view: rework attachment rendering logic with filters
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-28 11:14:05 +03:00
Andrei Zisu
c04b593bdf
Use BODY instead of RFC822
RFC3501 seems to prefer BODY attributes as the more modern equivalent
to RFC822. For example, this also allows us to use BODY.PEEK. Since the
fetch methods in melib are tightly coupled with the parser, we have to
add this here if we want to use BODY attributes during fetch.
2024-06-28 11:14:05 +03:00
Manos Pitsidianakis
0ffe7fa5b3
melib: add text/plain or text/html arg for text decoding
In the function that decodes attachments to text, the default was
Text::Plain. Now it's passed via an argument so that Text::Html can also
be used.
2024-06-28 11:14:05 +03:00
Andrei Zisu
b4579075a8
melib/imap: Allow XOAUTH2 string passed as string
For cases in which the user of melib already knows the token and auth
string and doesn't have to call an outside command.
2024-06-28 11:14:04 +03:00
Andrei Zisu
6f61176a99
Remove unecessary mut modifier
This also makes sense semantically since as_bytes shouldn't be
performing any mutations.
2024-06-28 11:14:01 +03:00
Manos Pitsidianakis
7bdc8f52b1
mail/listing: highlight_self also when self is sender
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-15 16:45:10 +03:00
Manos Pitsidianakis
7e8d19afc7
melib/email.rs: add Envelope::sender_any
Similar to Envelope::recipient_any, this method searches if its argument
exists in From: or Author: headers.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-15 16:45:09 +03:00
Manos Pitsidianakis
d3a45b3442
melib/notmuch: make default shared lib name a const
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-15 16:45:09 +03:00
Manos Pitsidianakis
27c4876fca
mail/listing: Prevent log flooding when drawing listing entries
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-15 16:45:09 +03:00
Manos Pitsidianakis
ff3fe07758
Fix new 1.79.0 clippy lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-15 16:45:09 +03:00
Damian Poddebniak
7856ea33f0 refactor: Transition more to imap-codec 2024-06-08 17:50:35 +03:00
Damian Poddebniak
a8956bafc8 chore: Update to imap-codec v2.0.0-alpha.1 2024-06-08 17:49:55 +03:00
Manos Pitsidianakis
f1332e82b6
Bump version to 0.8.6
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-08 11:55:13 +03:00
Manos Pitsidianakis
819551876d
view/envelope: fix decryption error not shown
Closes #409

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-05 19:46:38 +03:00
Manos Pitsidianakis
01bc62e0ad
melib/attachments: add new_plaintext method
Add new convenience method AttachmentBuilder::new_plaintext(Vec<u8>)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-05 19:46:38 +03:00
Manos Pitsidianakis
8dd87c1ac5
melib/email: add ContentType::is_text_plain()
Simple helper method to check if content-type is text/plain

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-03 10:12:26 +03:00
Manos Pitsidianakis
e9dd6becc3
.gitea/PULL_REQUEST_TEMPLATE.md: comment out content
I suspect this is annoying to see when opening a PR for everyone and not
just me, so I comment it out by default.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-02 15:15:34 +03:00
Manos Pitsidianakis
6be25ac3df
melib/jmap: don't use client field for get/posts
HTTP Requests should go through the get_async()/post_async() methods of
JmapConnection, which traces requests if optional feature `jmap-trace`
is enabled.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-02 15:15:34 +03:00
Manos Pitsidianakis
33999fc6ab
melib/jmap: re-add Submission to USING
Submission capability is necessary for Identity retrievals.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-02 15:15:34 +03:00
Manos Pitsidianakis
45bfcf8707
melib/email/headers/standards.rs: minor refactors
Refactor the standards! macro to remove unnecessary repetitions, and
also add RFC2183 as source for the Content-Disposition header.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-02 12:17:56 +03:00
Manos Pitsidianakis
3bab5324c4
melib/email: Improve Debug impl for ContentType etc
Improve Debug impl for ContentType, Text, ContentTransferEncoding which
all include bytes in some of their variants. The derived Debug
implementation did not show them as readable ascii strings, so a custom
impl was necessary to make it readable.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-06-02 12:15:33 +03:00
Manos Pitsidianakis
2bfe608678
view.rs: hide self from "add contacts" options
Adding ourselves to our contacts doesn't make sense (in the generic
case) so remove them from the list that is shown when the user selects
the add contacts shortcut.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-30 15:44:19 +03:00
Manos Pitsidianakis
e8e7697001
terminal: fix edge case with strings/linebreaking
When printing and line-breaking a string in CellBuffer::write_string(),
the line should continue from the "beginning" of the previous one.
However it might be different than the offset of the first character of
the string, because it may be indented already.

This would result in awkward line breaks like:

            Available actions: [ list- |# end of terminal/border
            post, list-unsubscribe, lis|
            t-archive ]

This commit adds an extra argument to write_string() to specify the x
offset of the string, so that we can apply it while printing characters
but ignore it when the line changes; then the x coordinate will take the
value of the line_break value.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-30 13:58:56 +03:00
Manos Pitsidianakis
74f0d12afb
tools: remove obsolete imapshell.rs and smtp_conn.rs
Previous commits added shells in the main binary.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 22:47:35 +03:00
Manos Pitsidianakis
571bd98497
Add proper imap-shell in tools subcommand for debugging
Add imap-shell to tools subcommand.

To use, run

  $ meli tools imap-shell <account-name>

Where account name is what you have in your config file.

For convenience, typing an invalid name will list all the valid names:

  $ meli tools imap-shell "asdf"
  The configuration file does not contain the account `asdf`. It contains the following:
  user@example.com
  work
  personal account
  Try again with a valid account name.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 22:46:08 +03:00
Manos Pitsidianakis
e187bb3f0d
Add tools subcommand with smtp shell for debugging
Add tools subcommand, with options to launch imap, smtp or jmap shells.
Currently only smtp is implemented, imap is working but with quirks.

To use, run

  $ meli tools smtp-shell <account-name>

Where account name is what you have in your config file.

For convenience, typing an invalid name will list all the valid names:

  $ meli tools smtp-shell "asdf"
  The configuration file does not contain the account `asdf`. It contains the following:
  user@example.com
  work
  personal account
  Try again with a valid account name.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 22:46:08 +03:00
Manos Pitsidianakis
74a3539f88
terminal/cells.rs: fix degenerate OOB cell access
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 21:16:59 +03:00
Manos Pitsidianakis
7eed944abc
melib/jmap: fix screwed up rfc8620 module split
I first split the rfc8620.rs into two files with an octopus merge but
getting fixups autosquashed in rebase was impossible with this approach.
So I made the split manually and botched the state of the two splitted
halves.

This commit removes extraneous code and the unused rfc8620.rs file.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 13:59:03 +03:00
euxane
3de4908d6b man(7): fix typo for toggle_expand_headers
Signed-off-by: euxane <euxane.trangirard@pacien.net>
2024-05-29 12:36:32 +02:00
Manos Pitsidianakis
f2b59a7633
jmap: add RequestUrlTemplate type
Add type that preserves both text (String) and parsed Url value for a
Url template.

Also add a test to catch regressions.

Closes #403 (JMAP: message body fetching broken on v0.8.5)

Fixes: 51e3f163d4 ("melib/jmap: Use Url instead of String in deserializing")
Resolves: https://git.meli-email.org/meli/meli/issues/403
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:13 +03:00
Manos Pitsidianakis
f7838b1ddf
jmap/rfc8620.rs: split to methods.rs and objects.rs
Split rfc8620 to two separate files.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
a78f3f261d
jmap/rfc8620: move submodules to jmap/
Part of simplifying jmap module hierarchy.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
50ecade74a
jmap: merge rfc8620/tests.rs to tests.rs
In anticipation of next commit which will flatten the rfc8620 module
(move submodules to root).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
77867aeed4
jmap: unwrap object module
jmap module hierarchy has been historically weird at places, because
some modules had been developed in separate bursts.

Flatten object module since it was not necessary.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
fc1122a2aa
jmap/mailbox.rs: rename to backend_mailbox.rs
To differentiate with the `Mailbox` object module.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
7ba7dc70c5
jmap: imports cleanup in all modules
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
Manos Pitsidianakis
dce3852fe5
jmap: add capabilities module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-29 00:51:12 +03:00
euxane
4722d7ccb8 docs/meli.conf: also mention server_password_command for jmap
This was already supported but not mentioned on the manual page.

Signed-off-by: euxane <euxane.trangirard@pacien.net>
2024-05-27 19:59:40 +02:00
Manos Pitsidianakis
f0d1b9cfd1
README.md: add ayllu mirror link
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-23 14:12:01 +03:00
Manos Pitsidianakis
ed5a6b04f4
terminal.rs: add a symbols range to is_emoji check
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-23 09:30:18 +03:00
Manos Pitsidianakis
58d7327130
notifications: change new mail text content
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-23 09:29:50 +03:00
Manos Pitsidianakis
0e1e5b9ea7
terminal: add support for Alternate Scroll Mode (xterm)
If mouse mode is on, using the mouse scroll wheel will scroll inside
meli.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-23 09:28:41 +03:00
Manos Pitsidianakis
06ec2790d0
view/envelope.rs: fix str slice index panic
Replace direct slice indexing with get(<range>)

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-22 15:13:18 +03:00
Manos Pitsidianakis
fe08d52a7f
terminal.rs: add force_text_emoji_presentation option
If true, text presentations of color symbols and emoji will be enforced
as much as possible.

Might not work on all non-text symbols and is experimental.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-20 16:21:11 +03:00
Manos Pitsidianakis
39fbb164ed
Change info_message_{next,prev} shortcuts to <, >
Alt-< and Alt-> might not work on all keyboards/terminals.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-20 10:35:38 +03:00
Manos Pitsidianakis
c65635ef60
melib/connections.rs: Fix compilation for macos
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-20 10:17:04 +03:00
Manos Pitsidianakis
4148aee59b
melib: refactor smtp,draft errors and email tests
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-20 10:17:04 +03:00
Manos Pitsidianakis
4bdfb3a31b
melib/connections.rs: disable Nagle's algorithm by default
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-18 12:16:47 +03:00
Manos Pitsidianakis
671d35e21e
melib: update mailin-embedded dependency to 0.8.2
Closes: #391

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-16 08:49:00 +02:00
Manos Pitsidianakis
a4ebe3b7d4
conf.rs: Add ErrorKind::Platform
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:32 +02:00
Manos Pitsidianakis
57e3e643a1
conversations.rs: remove excessive right padding in flags
Flags had too many spaces on its right side padding. This commit removes
it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:32 +02:00
Manos Pitsidianakis
a8c7582fa3
melib/imap: fix ENVELOPE parsing in untagged responses
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:31 +02:00
Manos Pitsidianakis
a9c3b151f1
listing.rs: impl highlight_self in all index styles
Add highlight_self to all listing styles (compact, conversations, plain,
thread).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:31 +02:00
Manos Pitsidianakis
1abce964c7
melib: add Envelope::recipient_any method
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 12:01:30 +02:00
Manos Pitsidianakis
735b44f286
Add 'highlight_self' theme attribute
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 11:59:31 +02:00
Manos Pitsidianakis
50ff16c44f
themes: add LIGHT, DARK constant theme keys
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-14 11:59:31 +02:00
Manos Pitsidianakis
9ca34a6864
Update MSRV to 1.70.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-07 11:49:19 +03:00
Manos Pitsidianakis
8fff740176
Update yanked zerocopy dependency
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 19:15:58 +03:00
Manos Pitsidianakis
8eaf03554f
Bump version to 0.8.5
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 18:37:30 +03:00
Manos Pitsidianakis
8ec6f22090
Use ShellExpandTrait::expand in more user-provided paths
ShellExpandTrait::expand was not used consistently, leading to only some
functionalities supporting things like tilde expansion.

Fixes #387

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 18:33:43 +03:00
Manos Pitsidianakis
b5ddc397df
terminal: remove unwrap() from get_events() loop
When exiting the app, the received value might be None.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-06 10:49:41 +03:00
Manos Pitsidianakis
46e40856ba
dialogs: fix UIConfirmationDialog highlight printing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-05 16:56:33 +03:00
Manos Pitsidianakis
35408b1689
pager.rs: run pager filter asynchronously
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis
5d915baa81
terminal/embedded: use Screen::resize instead of CellBuffer::resize
CellBuffer::resize does not update generation info and should only be
used from within Screen::resize

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis
684fae3ed8
terminal: copy old content to new buf when resizing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis
ab04189887
clippy: fix new warnings for 1.78.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:32 +03:00
Manos Pitsidianakis
36b7c00b97
clippy: Put doc text type names and co. in backtics
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis
3a5306e9dd
View manpages in pager inside meli
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis
89c7972e12
command/error.rs: add suggestions to BadValue variant
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-04 20:26:31 +03:00
Manos Pitsidianakis
8f3dee9b22
args.rs: extract mod manpages to standalone file
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:47:42 +03:00
Manos Pitsidianakis
660022ce23
docs: add mailaddr.7 manpage
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:21:47 +03:00
Manos Pitsidianakis
29cc1bce5b
Remove obsolete file melib/src/text/tables.rs.gz
Fixes #382

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-05-02 13:20:01 +03:00
Manos Pitsidianakis
bc1b65316d
conversations.rs: fix constant redrawing
self.force_draw was not reset back to false after drawing, so it was
constantly being redrawn until meli becomes unresponsive.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-26 09:41:13 +03:00
Manos Pitsidianakis
11a0586d56
Remove num_cpus dependency
Functionality already exists in standard library with std:🧵:available_parallelism()

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-20 16:17:01 +03:00
Manos Pitsidianakis
f70496f14c
Add codemeta.json
https://codemeta.github.io/

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-18 15:08:57 +03:00
Manos Pitsidianakis
8a16cf6db4
listing/thread: fix wrong column index crash
columns[0] was jused in every for loop instead of columns[n], which
would make the debug_assert_eq(area generation, column generation) panic

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-17 13:17:34 +03:00
Manos Pitsidianakis
11f3077b06
args: add more possible values for manpage names
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-14 21:47:02 +03:00
Manos Pitsidianakis
dedee908d1
Update notify dep from 4.0.17 to 6.1.1
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:48:30 +03:00
Manos Pitsidianakis
255e93764a
Update linkify dep from 0.8.1 to 0.10.0
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:48:25 +03:00
Manos Pitsidianakis
c5e9e67604
docs: add historical-manpages dir
Add some old manpages that may be of interest to users:

- maildir (5)
- mbox (5)
- mbox (5qmail)
- qmail-maildir (5)

Under meli/docs/historical-manpages/

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:19:15 +03:00
Manos Pitsidianakis
ae96038fbf
Make unicode-segmentation a hard dependency
meli/melib are UTF8 software, so we should have proper Unicode support.

A compile-time env var is added, `UNICODE_REGENERATE_TABLES` to force
network access and rebuild the cached unicode tables.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-11 21:15:47 +03:00
Manos Pitsidianakis
07072e2e3f
melib/thread: prevent panic if envelope is deleted
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-06 21:08:37 +03:00
Manos Pitsidianakis
aa5737a004
compose: prevent drawing pager on embedded mode
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-06 21:07:40 +03:00
Manos Pitsidianakis
48cb9ee204
Fix compilation for macos
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-04-05 16:27:25 +03:00
Guillaume Ranquet
c53a32de4c
thread: re-enables horizontal thread view
Re-implemnts horizontal thread view.
Default is still vertical, but pressing toggle_layout now has an effect.

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-04-05 16:00:58 +03:00
Manos Pitsidianakis
a69c674c07
Fix new 1.77 clippy lints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 16:04:51 +02:00
Manos Pitsidianakis
6a66afe93e
view: make add contact dialog scrollable on overflow
If contact entries in the add contact dialog are too many to fit in the
dialog area, show a scrollbar and allow the user to navigate it.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 15:21:05 +02:00
Manos Pitsidianakis
974502c6ff
melib/addressbook: impl Hash for Card
Implement hashing for Card.

This fixes the appearance of duplicate entries in the add contacts
selector in an envelope view when an address appears more than one time
in the envelope headers.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-24 15:14:20 +02:00
Manos Pitsidianakis
3e9144657b
meli: store children process metadata
Store children process metadata. Pid and command lines can then be shown
in the UI and in logs.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-23 10:43:58 +02:00
Manos Pitsidianakis
35a9f33aab
listing: extract common FlagString logic
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-21 21:04:22 +02:00
Manos Pitsidianakis
475609fe92
listing: make {prev,next}_entry shortcut behavior consistent
prev_entry, next_entry shortcuts (default bindings: Ctrl+p and Ctrl+n)
were not behaving consistently in all different listing index styles. In
particular in some conditions the switch entry shortcuts worked at most
once because the cursor position was not updated properly. This commit
fixes that.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-21 13:35:41 +02:00
Manos Pitsidianakis
38bca8f8bc
docs/meli.conf.5: mention use_oauth2=true for gmail oauth2
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-17 14:07:07 +02:00
Manos Pitsidianakis
ec01a4412a
melib/imap: turn some sync connections to unsync
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 23:47:30 +02:00
Manos Pitsidianakis
4e941a9e8b
accounts: add default_mailbox setting
Add a default mailbox setting:

> The mailbox that is the default to open / view for this account. Must be
> a valid mailbox name.
>
> If not specified, the default is [`Self::root_mailbox`].

Closes: #350
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:16:05 +02:00
Manos Pitsidianakis
742f038f74
accounts: move sent_mailbox to settings
In the next commits we will add a `default_mailbox` field. Instead of
poluting the `Account` struct with more setting fields, consolidate them
on its `settings`.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:51 +02:00
Manos Pitsidianakis
484712b0c3
accounts: check for unrecoverable errors in is_online
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:50 +02:00
Manos Pitsidianakis
264782d228
Various unimportant minor style/doc fixups
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 18:15:50 +02:00
Manos Pitsidianakis
41e965b8a3
meli/accounts: split mbox/job stuff in submodules
accounts.rs is getting rather long (almost 3K lines) so split standalone
stuff in submodules.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-16 12:48:57 +02:00
Manos Pitsidianakis
f31b5c4000
melib/connections: don't print raw bytes as escaped unicode
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 13:20:07 +02:00
Manos Pitsidianakis
8014af2563
imap/protocol_parser: reduce debug prints
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 13:19:26 +02:00
Manos Pitsidianakis
4ce616aeca
CI: fix lints.yaml rustup install step
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-15 11:42:58 +02:00
Manos Pitsidianakis
a3aaec382a
melib/conf: remove unused imports
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-14 19:18:23 +02:00
Manos Pitsidianakis
b8b24282a0
Update all instances of old domains with meli-email.org
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 21:38:12 +02:00
Manos Pitsidianakis
e481880321
Various manpage touchups and URL updates
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 21:34:46 +02:00
Geert Stappers
a88b8c5ea0 debian/changelog warning fix
Added
- actual change log entries
- a space in front of hyphen hyphen
- empty lines

Signed-off-by: Geert Stappers <stappers@stappers.it>
2024-03-10 16:43:51 +02:00
Manos Pitsidianakis
b820bd6d9c
melib/imap: remove unused imap_trace! and fix comp
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-10 13:08:58 +02:00
Manos Pitsidianakis
3b93fa8e7c
state.rs: don't draw messages above embedded terminal
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:47 +02:00
Manos Pitsidianakis
634bd1917a
melib/imap: convert log prints to traces
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:33 +02:00
Manos Pitsidianakis
b5fd3f57a7
listing.rs: make self.view an Option
Prevent accessing a ThreadView if it has not been initialized by making
an uninitialized ThreadView impossible.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:56:01 +02:00
Manos Pitsidianakis
1fcb1d59b8
build.rs: remove rerun when build.rs changes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-08 16:55:28 +02:00
Manos Pitsidianakis
e2cdebe89c
Add option to highlight self in mailing list threads
Use under `listing` options such as:

globally
========

  [listing]
  highlight_self = true

per-account
===========

  [accounts.work]
  root_mailbox = '[Gmail]'
  format = "imap"
  subscribed_mailboxes = ["*"]
  listing.index_style = "compact"
  listing.highlight_self = true

per-mailbox
===========

  [accounts.work.mailboxes]
  "INBOX/Lists/project-devel" = { listing.highlight_self=true }

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-06 17:39:28 +02:00
Manos Pitsidianakis
3884c0da1f
docs/meli.conf.5: small typographic fixups
- Add macro for literal string values to enable showing unicode
 literal characters
- Fix bool/boolean inconsistency
- Fix "true" / true inconsistency
- Add macro for horizontal rule in subsections
- Add terminal subsection about unicode modifier / combining marks for
  emojis

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-06 17:39:28 +02:00
Manos Pitsidianakis
26928e3ae9
terminal: fix compilation for macos
Fixes: 70fc2b455c ("Update nix dependency to 0.27")
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis
070930e671
meli/sqlite3: Fix auto index build when missing
An error was returned from the db_path function, preventing the issuing
of the reindex command in the background.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis
c7aee72525
melib: add clippy::doc_markdown
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 22:57:09 +02:00
Manos Pitsidianakis
30a3205e4f
meli: Add clippy::doc_markdown
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-03 11:38:57 +02:00
Manos Pitsidianakis
9af284b8db
listing: Don't hide unread count for mailboxes that are partly truncated
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-03-01 17:14:05 +02:00
Manos Pitsidianakis
62aee4644b
Add subcommand to print log file location
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-29 12:04:49 +02:00
Manos Pitsidianakis
5af2e1ee66
Add subcommand to print config file location
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-29 11:53:30 +02:00
Manos Pitsidianakis
4e7b665672
sqlite caching refactor
General refactoring to make blocking operations use special blocking
thread workers, SQL operations to use transactions, and setting up WAL
journal mode mode to minimize locking.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-24 19:16:42 +02:00
Manos Pitsidianakis
fd64fe0bf8
README.md: update codeberg.org URL
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-21 19:54:50 +02:00
Manos Pitsidianakis
51e3f163d4
melib/jmap: Use Url instead of String in deserializing
Catch invalid URLs at the parsing stage.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-13 14:13:53 +02:00
Manos Pitsidianakis
417b24cd84
meli: print invalid command on error
Instead of printing just "invalid command", print the command as well.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-13 14:13:52 +02:00
Manos Pitsidianakis
873a67d0fb
Replace erroneous use of set_err_kind with set_kind
set_err_kind() is a method of the IntoError trait, not an Error method;
it is meant to be used for any error type that can be converted into
Error. Since melib::Error implements Into<melib::Error> tautologically,
this was not a compilation error. Nevertheless, the correct thing to do
is use the type method directly to set ErrorKind.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis
c332c2f5ff
Fix new clippy lints (mostly clippy::blocks_in_conditions)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis
1048ce6824
melib/utils: add hostname() utility function
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis
70fc2b455c
Update nix dependency to 0.27
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis
8de8addd11
melib/datetime: add cfg for musl builds
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-11 17:13:05 +02:00
Manos Pitsidianakis
1fe3619208
conf: Make conf validation recognize AccountSettings extra keys
AccountSettings extra keys like `vcard_folder` were not taken into
account when validating a config.

This commit introduces an AccountSettings::validate_config() method that
checks for the presence and validity of this key value.

Fixes #349

https://git.meli-email.org/meli/meli/issues/349

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-04 14:52:06 +02:00
Manos Pitsidianakis
0b468d88ad
addressbook/vcard: improve Error messages
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-02-04 14:52:06 +02:00
Manos Pitsidianakis
1eca34b398
Set lowest priority to shortcut command UIEvents
Commit (a37d5fc1 conf/shortcuts: implement
a key to command mapping) introduced shortcuts that expand to user
defined commands. To allow already existing shortcuts to take
precedence, the check for the user-defined shortcuts should be the last
one in the evaluation order.

Example problem scenario:
- Press new_mail shortcut (e.g. `m`)
- Code in listing.rs searches if it matches any of the commands, and
  regardless if it matches or not, stops the evaluation and returns.
- New mail composer never shows up.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-21 12:03:40 +02:00
Manos Pitsidianakis
5afc078587
Update README.md, DEVELOPMENT.md and create BUILD.md
README.md is quite lengthy so split extraneous info to other `.md`
files.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-20 12:09:34 +02:00
Guillaume Ranquet
a37d5fc1d1 conf/shortcuts: implement a key to command mapping
Permits users to map keys in their configuration file to an array of meli commands

e.g:
[shortcuts.listing]
commands = [ { command = [ "tag remove trash", "flag unset trash" ], shortcut = "D" },
             { command = [ "tag add trash", "flag set trash" ], shortcut = "d" } ]

Signed-off-by: Guillaume Ranquet <granquet@baylibre.com>
2024-01-18 15:53:35 +01:00
Manos Pitsidianakis
60f26f9dae
melib: Fix some old pre-intradoc rustdoc links
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-11 09:22:28 +02:00
Ethra
e80ea9c9de
Changed default manpage install path 2024-01-11 05:08:58 +03:00
Manos Pitsidianakis
64e60cb0ee
listing: fix select modifier regression
Commit 61a0c3c27f ("listing: do not clear
selection after action") broke select/jump modifiers (e.g. prefixing a
jump with a number).

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-08 12:32:50 +02:00
Manos Pitsidianakis
81d1c0536b
scripts: add mandoc_lint.sh
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 16:22:16 +02:00
Manos Pitsidianakis
cd448924ed
listing: add clear-selection command
Add a command that performs what Escape does: clears the selection.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 15:35:56 +02:00
Manos Pitsidianakis
61a0c3c27f
listing: do not clear selection after action
Clear selection only when Escape is pressed, not after action is
completed. The user might want to perform further actions on the
selection.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-06 15:20:00 +02:00
Manos Pitsidianakis
7952006870
melib/percent_encoding: remove doctests, add tests module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-04 10:41:00 +02:00
Manos Pitsidianakis
ddab3179c2
melib/wcwidth: move tests to tests module
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-04 10:40:31 +02:00
Manos Pitsidianakis
7861fb0402
Fix typos found with typos tool
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-03 11:08:55 +02:00
Manos Pitsidianakis
148f0433d9
meli: implement flag set/unset action in UI
Also document it in manpages meli.1 and meli.7

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 21:28:21 +02:00
Manos Pitsidianakis
8185f2cf7d
meli: add deny clippy lints and fix them
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 15:59:13 +02:00
Manos Pitsidianakis
0270db0123
melib: From<&[u8]> -> From<B: AsRef<[u8]>>
This change allows byte literals to be used with the from trait method.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 15:16:47 +02:00
Manos Pitsidianakis
8ddd673dd8
melib/imap/untagged: update all mailboxes
When receiving an envelope event (deleted, or changed flags), update all
mailboxes that contain that envelope hash; not just the currently
selected mailbox.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:36:29 +02:00
Manos Pitsidianakis
e3351d2755
melib/imap: fix set unseen updating all mboxes
When manually setting an envelope as not seen, all mailboxes had their
unseen count increased. This commit updates only those that include the
envelope in the first place.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:34:50 +02:00
Manos Pitsidianakis
31401fa35c
melib/backends: add LazyCountSet::contains method
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 14:34:05 +02:00
Manos Pitsidianakis
33408146a1
Fix feature permutation mis-compilations found with cargo-hack
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 11:38:42 +02:00
Manos Pitsidianakis
8a95febb78
CI: set debuginfo=0 in test/lint builds
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 11:03:29 +02:00
Manos Pitsidianakis
73d5b24e98
melib/tests: merge integration tests in one crate
Saves about 0.5 seconds from compilation and runtime.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2024-01-01 10:44:31 +02:00
Manos Pitsidianakis
0da97dd8c1
mail/listing: check row_updates in is_dirty()
If there are row_updates, it means we need to redraw. But in the draw()
call, we check is_dirty() to decide whether to proceed drawing. Add
row_updates not being empty into the dirty conditions.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 20:02:58 +02:00
Manos Pitsidianakis
933bf157ae
melib/email/parser: ack \ as an atom
I think this is not spec compliant but the MIME spec (rfc6068 - The
'mailto' URI Scheme) uses it for "valid" addresses.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis
f685726eac
melib/email/parser: add backtrace field to ParsingError
Add backtrace field to ParsingError when the build is for testing or
documentation.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis
ab1b946fd9
melib/error: don't print details if it's an empty string.
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:56:52 +02:00
Manos Pitsidianakis
ce4ba06ce9
command: add a flag set/unset command
e.g. "flag unset draft"

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis
bebb473d1b
melib/mbox: derive extra traits for enums
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis
f0866a3965
meli: make config error more user-friendly
If `send_mail` is incorrect, display a long-ish list of valid examples.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:45 +02:00
Manos Pitsidianakis
f63774fa6d
Fix new clippy lints (1.75)
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-29 19:51:44 +02:00
Manos Pitsidianakis
808aa4942d
melib: rename text_processing to text for the whole brevity thing
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-26 16:47:42 +02:00
Manos Pitsidianakis
08518e1ca8
terminal: remove obsolete position.rs module
The functions in terminal::position were pretty much obsolete after
commit

0e3a0c4b70 Add safe UI widget area drawing API

So this commit does a little cleanup and removes the module.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:49:23 +02:00
Manos Pitsidianakis
34a2d52e7e
Fix rustdoc::redundant_explicit_links
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:14:00 +02:00
Manos Pitsidianakis
4026e25428
melib/notmuch: add some doc comments
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:13:15 +02:00
Manos Pitsidianakis
ca7d7bb95d
melib/notmuch: use message freeze/thaw for flag changes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 19:12:45 +02:00
Manos Pitsidianakis
ebe1b3da7e
melib/notmuch: wrap *mut struct fields in NonNull<_>
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:50:23 +02:00
Manos Pitsidianakis
506ae9f594
melib/error: Add ErrorKind::LinkedLibrary variant
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:43:12 +02:00
Manos Pitsidianakis
b6f769b2f4
mail/listing: add field names to row_attr! bool values
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:35:09 +02:00
Manos Pitsidianakis
3691cd2962
accounts.rs: send EnvelopeUpdate event after self.collection.update_flags()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-17 18:20:14 +02:00
Manos Pitsidianakis
97eb636375
Makefile: add dpkg --print-architecture to deb filename
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-13 15:43:00 +02:00
Manos Pitsidianakis
b3079715f6
melib/smtp: disable flakey test_smtp()
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-13 09:45:17 +02:00
Manos Pitsidianakis
86bbf1ea57
melib/notmuch: refresh NotmuchMailbox counts when setting flags
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-12 20:03:14 +02:00
Manos Pitsidianakis
1b0bdd0a9a
melib/notmuch: split queries and mailbox into submodules
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-12 20:03:14 +02:00
Manos Pitsidianakis
7412c23870
Bump meli version to 0.8.5-rc.3
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-11 13:18:50 +02:00
Manos Pitsidianakis
500fe7f7e4
Update CHANGELOG.md
Use git-cliff.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-11 13:13:19 +02:00
Manos Pitsidianakis
2419f4bd40
CI: add debian package build workflow
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-11 09:32:40 +02:00
Manos Pitsidianakis
59c99fdc79
debian: update debian package metadata
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 21:22:23 +02:00
Manos Pitsidianakis
5f8d7c8039
debian: Update deb-dist target command with author metadata
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 19:38:29 +02:00
Manos Pitsidianakis
876616d45b
CI: use actions/upload-artifact@v3
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:56:38 +02:00
Manos Pitsidianakis
c41f35fdd5
CI: use actions/checkout@v3
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:56:22 +02:00
Manos Pitsidianakis
773254864b
CI: remove on-push hooks for actions w/ run on-pr
Because it results in jobs being scheduled twice, once because of push
and once because they are in a PR.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:51:03 +02:00
Manos Pitsidianakis
e19f3e572c
Cargo-sort all Cargo.toml files
With:

cargo sort  --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace meli
cargo sort  --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace melib
cargo sort  --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace tools
cargo sort  --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace fuzz

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:33:10 +02:00
Manos Pitsidianakis
1617212c5b
CI: add scripts/check_debian_changelog.sh lint
Check if latest version in debian/changelog matches the version in
meli/Cargo.toml.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:33:10 +02:00
Manos Pitsidianakis
3ba1603af2
CI: add manifest file only lints workflow
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-10 15:33:10 +02:00
Manos Pitsidianakis
0a617410ec
CI: split test.yaml to test.yaml and lints.yaml
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 22:08:38 +02:00
Manos Pitsidianakis
5ff4e8ae68
CI: run builds.yaml when any manifest file changes
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 22:08:38 +02:00
Manos Pitsidianakis
c4344529e3
Add .git-blame-ignore-revs file
See DEVELOPMENT.md for info.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 21:30:02 +02:00
Manos Pitsidianakis
f900dbea46
Use cargo-derivefmt to sort derives alphabetically
Used https://github.com/dcchut/cargo-derivefmt

With command:

cargo install --locked \
--git https://github.com/dcchut/cargo-derivefmt \
--bin cargo-derivefmt \
--rev 2ff93de7fb418180458dd1ba27e5655607c23ab6

Since it's not on crates.io at the moment.

Sample diff:

  -#[derive(Debug, Deserialize, Clone, Serialize)]
  +#[derive(Clone, Debug, Deserialize, Serialize)]

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 21:28:12 +02:00
Manos Pitsidianakis
f3e85738e7
meli: move build.rs scripts to build directory
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 21:24:18 +02:00
Manos Pitsidianakis
3a70979483
Update minimum rust version from 1.65.0 to 1.68.2
Found with `cargo msrv --bisect --min 1.67.0`

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 18:01:12 +02:00
Manos Pitsidianakis
24971d1960
Fix compilation with 1.70.0 cargo
Cargo bug: https://github.com/rust-lang/cargo/issues/10788

Caused meli to not be able to be installed with 1.70.0 cargo.

This commit expresses the static dependencies differently to allow both
1.70.0 and later versions understand the optional dependency feature
activation.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 17:28:05 +02:00
Manos Pitsidianakis
e37997d697
mail/view: store Link URL value in Link type
Due to changes in how decoded email body is stored in `ViewFilter`s,
the previous way of accessing URL values (by using the `start` and `end`
offset in the e-mail body) meant the offset values might not be correct.

Store the value right away to prevent this from happening.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-09 15:08:56 +02:00
Manos Pitsidianakis
3adba40e32
scripts/make_html_manual_page.py: add macos manpage mirror url
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-06 16:33:19 +02:00
Manos Pitsidianakis
da251455a0
Bump meli version to 0.8.5-rc.2
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-04 17:25:22 +02:00
Manos Pitsidianakis
d16afc7d8d
Bump version to 0.8.5-rc.2
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-04 17:22:56 +02:00
Manos Pitsidianakis
7eedd86051
listings: remove address_list! macro
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-04 16:29:33 +02:00
Manos Pitsidianakis
c751b2e845
Re-enable conversations listing style
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-04 16:22:53 +02:00
Manos Pitsidianakis
031d0f7dc7
terminal: add area.is_empty() checks in cell iterators
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-04 16:20:43 +02:00
Manos Pitsidianakis
2c6f180df9
meli/notifications: fix macos compilation
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-03 18:49:14 +02:00
Manos Pitsidianakis
63a63253d7
melib/datetime: use type alias for c_char
On arm64, it's u8, not i8.

Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-03 18:08:48 +02:00
Manos Pitsidianakis
71f3ffe740
Update Makefile
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-03 18:08:48 +02:00
Manos Pitsidianakis
10c3b0eabe
Bump version to 0.8.5-rc.1
Signed-off-by: Manos Pitsidianakis <manos@pitsidianak.is>
2023-12-02 19:20:34 +02:00
Andrei Zisu
64898a0583
melib/imap: Make UIDStore constructor pub
I honestly forget exactly why this change is needed, so I need to
recheck.
2023-12-02 19:05:13 +02:00
Andrei Zisu
77a8d9e2c2
melib: Make ModSequence publicly accessible
This way it can be imported from this namespace in depending code.
2023-12-02 19:00:48 +02:00
344 changed files with 58338 additions and 31849 deletions

View file

@ -1,2 +0,0 @@
[env]
PCRE2_SYS_STATIC = "1"

2
.git-blame-ignore-revs Normal file
View file

@ -0,0 +1,2 @@
# Use cargo-derivefmt to sort derives alphabetically
f900dbea468e822c5a510a72ecc6367549443927

37
.gitea/Makefile.build Normal file
View file

@ -0,0 +1,37 @@
# SPDX-License-Identifier: EUPL-1.2
#
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
.POSIX:
.SUFFIXES:
CARGO_INCREMENTAL ?= 0
CARGO_NET_RETRY ?= 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
RUSTUP_MAX_RETRIES ?= 10
RUST_BACKTRACE ?= short
.PHONY: all
all: cargo-check cargo-test-compiles cargo-test rustdoc-build rustdoc-test
@printf "All completed.\n"
.PHONY: cargo-check
cargo-check:
@printf "cargo-check\n"
cargo check --all-features --all --tests --examples --benches --bins
.PHONY: cargo-test-compiles
cargo-test-compiles:
@printf "cargo-test-compiles\n"
cargo test --all --no-fail-fast --all-features --no-run --locked
.PHONY: cargo-test
cargo-test:
@printf "cargo-test\n"
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
.PHONY: rustdoc-build
rustdoc-build:
@printf "rustdoc-build\n"
env DISPLAY= WAYLAND_DISPLAY= make build-rustdoc
.PHONY: rustdoc-test
rustdoc-test:
@printf "rustdoc-test\n"
make test-docs

61
.gitea/Makefile.lint Normal file
View file

@ -0,0 +1,61 @@
# SPDX-License-Identifier: EUPL-1.2
#
# Makefile for: "Run cargo lints" workflow, in .gitea/workflows/lints.yaml
.POSIX:
.SUFFIXES:
CARGO_INCREMENTAL ?= 0
CARGO_NET_RETRY ?= 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
RUSTUP_MAX_RETRIES ?= 10
RUST_BACKTRACE ?= short
NIGHTLY_EXISTS=`((cargo +nightly 2> /dev/null 1> /dev/null) && echo 0)|| echo 1)`
GIT=env GIT_CONFIG_GLOBAL="" GIT_CONFIG_SYSTEM="" GIT_CONFIG_NOSYSTEM=1 git
.PHONY: all
all: cargo-msrv rustfmt clippy cargo-derivefmt-melib cargo-derivefmt-meli cargo-derivefmt-tools
@printf "All checks completed.\n"
# Check both melib and meli in the same Make target, because if melib does not
# satisfy MSRV then meli won't either, since it depends on melib.
.PHONY: cargo-msrv
cargo-msrv:
@printf "cargo-msrv\n"
cargo msrv --output-format json --log-level trace --log-target stdout --path meli verify -- cargo check --all-targets
cargo msrv --output-format json --log-level trace --log-target stdout --path melib verify -- cargo check --all-targets
.PHONY: rustfmt
rustfmt:
@printf "rustfmt\n"
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then printf "running rustfmt with nightly toolchain\n"; else printf "running rustfmt with active toolchain\n"; fi))
@((if [ "${NIGHTLY_EXISTS}" -eq 0 ]; then cargo +nightly fmt --check --all; else cargo fmt --check --all; fi))
.PHONY: clippy
clippy:
@printf "clippy\n"
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins
.PHONY: cargo-derivefmt-melib
cargo-derivefmt-melib:
@printf "cargo-derivefmt-melib\n"
@printf "Checking that derives are sorted alphabetically...\n"
cargo derivefmt --manifest-path ./melib/Cargo.toml
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
@($(GIT) diff --quiet ./melib && $(GIT) diff --cached --quiet ./melib && printf "All ./melib derives are sorted alphabetically.\n") || (printf "Some derives in the ./melib crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
.PHONY: cargo-derivefmt-meli
cargo-derivefmt-meli:
@printf "cargo-derivefmt-meli\n"
@printf "Checking that derives are sorted alphabetically...\n"
cargo derivefmt --manifest-path ./meli/Cargo.toml
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
@($(GIT) diff --quiet ./meli && $(GIT) diff --cached --quiet ./meli && printf "All ./meli derives are sorted alphabetically.\n") || (printf "Some derives in the ./meli crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)
.PHONY: cargo-derivefmt-tools
cargo-derivefmt-tools:
@printf "cargo-derivefmt-tools\n"
@printf "Checking that derives are sorted alphabetically...\n"
cargo derivefmt --manifest-path ./tools/Cargo.toml
@$(GIT) checkout --quiet meli/src/conf/overrides.rs
@($(GIT) diff --quiet ./tools && $(GIT) diff --cached --quiet ./tools && printf "All ./tools derives are sorted alphabetically.\n") || (printf "Some derives in the ./tools crate are not sorted alphabetically, see diff:\n"; $(GIT) diff HEAD; exit 1)

View file

@ -0,0 +1,28 @@
# SPDX-License-Identifier: EUPL-1.2
#
# Makefile for: "Build and run Tests" workflow, in .gitea/workflows/build.yaml
.POSIX:
.SUFFIXES:
CARGO_INCREMENTAL ?= 0
CARGO_NET_RETRY ?= 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL ?= sparse
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0
RUSTUP_MAX_RETRIES ?= 10
RUST_BACKTRACE ?= short
.PHONY: all
all: cargo-sort check-debian-changelog
@printf "All checks completed.\n"
.PHONY: cargo-sort
cargo-sort:
@printf "cargo-sort\n"
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace fuzz
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace tools
cargo-sort --check --check-format --grouped --order package,bin,lib,dependencies,features,build-dependencies,dev-dependencies,workspace --workspace
.PHONY: check-debian-changelog
check-debian-changelog:
@printf "Check debian/changelog is up-to-date.\n"
./scripts/check_debian_changelog.sh

View file

@ -1,25 +1,41 @@
---
name: "Pull Request"
about: "Standard pull request template."
title: "WIP: "
about: "Basic pull request template"
ref: "master"
---
<!-- If your PR is ready to merge/review, remove the `WIP: ` prefix from the title. -->
<!-- If your PR is not ready to merge/review, you can add a `WIP: ` prefix to the title. -->
<!--
This template is just a suggestion, and is commented out using HTML comment syntax.
It will not show up in your PR text unless you remove the comment markers.
-->
### Summary of the PR
<!--
<!-- Changes introduced in this PR. -->
## Summary of the PR
### Requirements
Changes introduced in this PR.
Before submitting your PR, please make sure you have addressed the following requirements:
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit has a message describing the motivation behind the change, if appropriate.
* [ ] All added/changed public-facing functionality, especially configuration options, are documented in the manual pages.
## Requirements
Before submitting your PR, please make sure you have addressed the following
requirements:
* [ ] All commits in this PR are signed (with `git commit -s`), and the commit
has a message describing the motivation behind the change, if
appropriate.
* [ ] All added/changed public-facing functionality, especially configuration
options, are documented in the manual pages.
* [ ] Any newly added `unsafe` code is properly documented.
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the project root.
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project root.
* [ ] Each commit does not break any test. Run `make test` in the project root. If you have `cargo-nextest` installed, you can run `cargo nextest run --all --no-fail-fast --all-features --future-incompat-report` instead.
* [ ] Each commit has been formatted with `rustfmt`. Run `make fmt` in the
project root.
* [ ] Each commit has been linted with `clippy`. Run `make lint` in the project
root.
* [ ] Each commit does not break any test. Run `make test` in the project root.
If you have `cargo-nextest` installed, you can run `cargo nextest run
--all --no-fail-fast --all-features --future-incompat-report` instead.
-->

109
.gitea/check_dco.sh Executable file
View file

@ -0,0 +1,109 @@
#!/usr/bin/env sh
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
# Lint with shellcheck -s sh -S style check_dco.sh
# Notes:
# ======
#
# - We need to make sure git commands do not read from any existing configs to
# prevent surprises like default trailers being added.
# - we need to pass `--always` to `git-format-patch` to check even empty
# commits despite them not being something we would merge. This tripped me up
# when debugging this workflow because I tested it with empty commits. My
# fault.
export GIT_CONFIG_GLOBAL=""
export GIT_CONFIG_SYSTEM=""
export GIT_CONFIG_NOSYSTEM=1
ensure_env_var() {
set | grep -q "^${1}=" || (printf "Environment variable %s missing from process environment, exiting.\n" "${1}"; exit "${2}")
}
ensure_env_var "GITHUB_BASE_REF" 1 || exit $?
ensure_env_var "GITHUB_HEAD_REF" 2 || exit $?
# contains_correct_signoff() {
# author=$(git log --author="$1" --pretty="%an <%ae>" -1)
# git format-patch --always --stdout "${1}^..${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: ${author}"
# }
contains_signoff() {
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${1}" | git interpret-trailers --parse | grep -q "^Signed-off-by: "
}
get_commit_sha() {
if OUT=$(git rev-parse "${1}"); then
printf "%s" "${OUT}"
return
fi
printf "Could not git-rev-parse %s, falling back to HEAD...\n" "${1}" 1>&2
git rev-parse HEAD
}
echo "Debug workflow info:"
echo "Base ref GITHUB_BASE_REF=${GITHUB_BASE_REF}"
echo "Head ref GITHUB_HEAD_REF=${GITHUB_HEAD_REF}"
BASE_REF=$(get_commit_sha "${GITHUB_BASE_REF}")
HEAD_REF=$(get_commit_sha "${GITHUB_HEAD_REF}")
echo "Processed base ref BASE_REF=${BASE_REF}"
echo "Processed head ref HEAD_REF=${HEAD_REF}"
RANGE="${BASE_REF}..${HEAD_REF}"
echo "Range to examine is RANGE=${RANGE}"
if ! SHA_LIST=$(git rev-list "${RANGE}"); then
printf "Could not get commit range %s with git rev-list, bailing out...\n" "${RANGE}"
exit 0
fi
echo "SHA list to examine is SHA_LIST="
echo "---------------------------------------------------------------------"
echo "${SHA_LIST}"
echo "---------------------------------------------------------------------"
echo ""
echo "Starting checks..."
output=$(printf "%s" "${SHA_LIST}" | while read -r commit_sha; do
contains_signoff_result=""
contains_signoff "${commit_sha}"; contains_signoff_result="$?"
if [ "${contains_signoff_result}" -ne 0 ]; then
printf "Commit does not contain Signed-off-by git trailer: %s\n\n" "${commit_sha}"
echo "patch was:"
echo "---------------------------------------------------------------------"
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}"
echo "---------------------------------------------------------------------"
echo "trailers were:"
echo "---------------------------------------------------------------------"
GIT_CONFIG_GLOBAL="" git format-patch --always -1 --stdout "${commit_sha}" | git interpret-trailers --parse
echo "---------------------------------------------------------------------"
echo "commit was:"
echo "---------------------------------------------------------------------"
git log --no-decorate --pretty=oneline --abbrev-commit -n 1 "${commit_sha}"
echo "---------------------------------------------------------------------"
fi
done)
if [ "${output}" = "" ]; then
exit 0
fi
echo "One or more of your commits in this Pull Request lack the Developer Certificate of Origin "
echo "which is more commonly known as DCO or the \"Signed-off-by: \" trailer line in the "
echo "git commit message."
echo "For information, documentation, help, check: https://wiki.linuxfoundation.org/dco"
echo "The reported errors were:"
printf "%s\n" "${output}" 1>&2
echo ""
echo "Solution:"
echo ""
echo "- end all your commits with a 'Signed-off-by: User <user@localhost>' line, "
echo " with your own display name and email address."
echo "- Make sure the signoff is separated by the commit message body with an empty line."
echo "- Make sure the signoff is the last line in your commit message."
echo "- Lastly, make sure the signoff matches your git commit author name and email identity."
exit 1

123
.gitea/workflows/build.yaml Normal file
View file

@ -0,0 +1,123 @@
# SPDX-License-Identifier: EUPL-1.2
name: Build and run Tests
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
pull_request:
paths:
- '.gitea/**'
- 'melib/src/**'
- 'melib/Cargo.toml'
- 'meli/src/**'
- 'meli/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
test:
name: Run tests
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, linux-arm64]
include:
- build: linux-amd64
arch: amd64
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
- build: linux-arm64
arch: arm64
os: ubuntu-latest-arm64
rust: stable
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev make
- name: Cache rustup
id: cache-rustup
uses: actions/cache@v4
with:
path: |
~/.rustup/
~/.cargo/env
~/.cargo/config.toml
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: build-workflow-${{ matrix.build }}-rustup
- id: rustup-setup
if: steps.cache-rustup.outputs.cache-hit != 'true'
name: Install rustup and toolchains
shell: bash
run: |
if ! command -v rustup &>/dev/null; then
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
fi
- name: Source .cargo/env
shell: bash
run: |
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
- name: Setup Rust target
if: steps.cache-rustup.outputs.cache-hit != 'true'
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- name: Add test dependencies
if: steps.cache-rustup.outputs.cache-hit != 'true'
run: |
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
- name: Restore build artifacts cache in target dir
id: cache-deps
uses: actions/cache/restore@v4
with:
path: target/
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: cargo-check
run: |
make -f ./.gitea/Makefile.build cargo-check
- if: steps.cache-deps.outputs.cache-hit != 'true'
name: Save build artifacts in target dir
id: save-cache-deps
uses: actions/cache/save@v4
with:
path: target/
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: cargo-test-compiles
if: success() || failure()
run: |
make -f ./.gitea/Makefile.build cargo-test-compiles
- name: cargo-test
run: |
make -f ./.gitea/Makefile.build cargo-test
- name: rustdoc build
if: success() || failure()
run: |
make -f ./.gitea/Makefile.build rustdoc-build
- name: rustdoc tests
if: success() || failure()
run: |
make -f ./.gitea/Makefile.build rustdoc-test

View file

@ -0,0 +1,79 @@
# SPDX-License-Identifier: EUPL-1.2
name: Build release binaries
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
push:
tags:
- v*
jobs:
build:
name: Build release binary
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, linux-arm64]
include:
- build: linux-amd64
arch: amd64
os: ubuntu-latest
rust: stable
artifact_name: 'meli-linux-amd64'
target: x86_64-unknown-linux-gnu
- build: linux-arm64
arch: arm64
os: ubuntu-latest-arm64
rust: stable
artifact_name: 'meli-linux-arm64'
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
- id: rustup-setup
name: Install rustup and toolchains
shell: bash
run: |
if ! command -v rustup &>/dev/null; then
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
fi
- name: Setup Rust target
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- name: Build binary
run: |
VERSION=$(grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
echo "VERSION=${VERSION}" >> $GITHUB_ENV
make
mkdir artifacts
mv target/*/release/* target/ || true
mv target/release/* target/ || true
mv target/meli artifacts/meli-${VERSION}-${{ matrix.target }}
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact_name }}-${{ env.VERSION }}
path: artifacts/meli-${{ env.VERSION }}-${{ matrix.target }}
if-no-files-found: error
retention-days: 30

View file

@ -0,0 +1,71 @@
# SPDX-License-Identifier: EUPL-1.2
name: Build .deb package
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
push:
tags:
- v*
jobs:
build-debian:
name: Create debian package
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, linux-arm64]
include:
- build: linux-amd64
arch: amd64
os: ubuntu-latest
rust: stable
artifact_name: 'linux-amd64'
target: x86_64-unknown-linux-gnu
- build: linux-arm64
arch: arm64
os: ubuntu-latest-arm64
rust: stable
artifact_name: 'linux-arm64'
target: aarch64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y mandoc debhelper quilt build-essential
- id: rustup-setup
name: Install rustup and toolchains
shell: bash
run: |
if ! command -v rustup &>/dev/null; then
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
rustup toolchain install --profile minimal ${{ matrix.rust }} --target ${{ matrix.target }}
rustup default ${{ matrix.rust }}
fi
- name: Build binary
run: |
VERSION=$(grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
make deb-dist
mkdir artifacts
echo "VERSION=${VERSION}" >> $GITHUB_ENV
mv ../meli_*.deb artifacts/meli-${VERSION}-${{ matrix.artifact_name }}.deb
- name: Upload Artifacts
uses: actions/upload-artifact@v3
with:
name: meli-${{env.VERSION}}-${{ matrix.artifact_name }}.deb
path: artifacts/meli-${{env.VERSION}}-${{ matrix.artifact_name }}.deb
if-no-files-found: error
retention-days: 30

View file

@ -1,86 +0,0 @@
name: Build release binary
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
push:
tags:
- v*
jobs:
build:
name: Build on ${{ matrix.build }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, ]
include:
- build: linux-amd64
os: ubuntu-latest
rust: stable
artifact_name: 'meli-linux-amd64'
target: x86_64-unknown-linux-gnu
steps:
- uses: https://github.com/actions/checkout@v2
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
#- id: cache-rustup
# name: Cache Rust toolchain
# uses: https://github.com/actions/cache@v3
# with:
# path: ~/.rustup
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
- name: Install Rust ${{ matrix.rust }}
uses: https://github.com/actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
target: ${{ matrix.target }}
override: true
- name: Configure cargo data directory
# After this point, all cargo registry and crate data is stored in
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
# that are needed during the build process. Additionally, this works
# around a bug in the 'cache' action that causes directories outside of
# the workspace dir to be saved/restored incorrectly.
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
#- id: cache-cargo
# name: Cache cargo configuration and installations
# uses: https://github.com/actions/cache@v3
# with:
# path: ${{ env.CARGO_HOME }}
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
- name: Setup Rust target
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- name: Build binary
run: |
make
mkdir artifacts
mv target/*/release/* target/ || true
mv target/release/* target/ || true
mv target/meli artifacts/
- name: Upload Artifacts
uses: https://github.com/actions/upload-artifact@v2
with:
name: ${{ matrix.artifact_name }}
path: artifacts
if-no-files-found: error
retention-days: 1

View file

@ -0,0 +1,26 @@
# SPDX-License-Identifier: EUPL-1.2
name: Verify DCO
on:
pull_request:
jobs:
test:
name: Verify DCO signoff on commit messages
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, ]
include:
- build: linux-amd64
os: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: check-dco
shell: sh
name: Check that commit messages end with a Signed-off-by git trailer
run: |
env GITHUB_BASE_REF="origin/${{env.GITHUB_BASE_REF}}" GITHUB_HEAD_REF="origin/${{env.GITHUB_HEAD_REF}}" sh ./.gitea/check_dco.sh

140
.gitea/workflows/lints.yaml Normal file
View file

@ -0,0 +1,140 @@
# SPDX-License-Identifier: EUPL-1.2
name: Run cargo lints
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
pull_request:
paths:
- '.gitea/**'
- 'melib/src/**'
- 'melib/Cargo.toml'
- 'meli/src/**'
- 'meli/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
lints:
name: Run lints
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, ]
include:
- build: linux-amd64
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
- name: Find meli MSRV from meli/Cargo.toml.
run: |
echo MELI_MSRV=$(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1) >> $GITHUB_ENV
printf "Rust MSRV is %s\n" $(grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1)
- name: Cache rustup
id: cache-rustup
uses: actions/cache@v4
with:
path: |
~/.rustup/
~/.cargo/env
~/.cargo/config.toml
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: lints-workflow-${{ matrix.build }}-rustup
- id: rustup-setup
if: steps.cache-rustup.outputs.cache-hit != 'true'
name: Install Rustup and toolchains
shell: bash
run: |
if ! command -v rustup &>/dev/null; then
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ env.MELI_MSRV }}"
rustup component add rustfmt --toolchain ${{ env.MELI_MSRV }}-${{ matrix.target }}
rustup toolchain install --profile minimal --component clippy,rustfmt --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
rustup default ${{ matrix.rust }}
fi
- name: Source .cargo/env
shell: bash
run: |
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
- name: Setup Rust target
if: steps.cache-rustup.outputs.cache-hit != 'true'
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- name: Add lint dependencies
if: steps.cache-rustup.outputs.cache-hit != 'true'
shell: bash
run: |
cargo install --version 0.15.1 --target "${{ matrix.target }}" cargo-msrv
# "This package is currently implemented using rust-analyzer internals, so cannot be published on crates.io."
RUSTFLAGS="" cargo install --locked --target "${{ matrix.target }}" --git https://github.com/dcchut/cargo-derivefmt --rev 95da8eee343de4adb25850893873b979258aed7f --bin cargo-derivefmt
- name: Restore build artifacts cache in target dir
id: cache-deps
uses: actions/cache/restore@v4
with:
path: target/
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: clippy
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f .gitea/Makefile.lint clippy
- if: steps.cache-deps.outputs.cache-hit != 'true'
name: Save build artifacts in target dir
id: save-cache-deps
uses: actions/cache/save@v4
with:
path: target/
key: workflow-${{ matrix.build }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: cargo-msrv verify melib MSRV
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f ./.gitea/Makefile.lint cargo-msrv
- name: rustfmt
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f .gitea/Makefile.lint rustfmt
- name: cargo-derivefmt melib
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f .gitea/Makefile.lint cargo-derivefmt-melib
- name: cargo-derivefmt meli
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f .gitea/Makefile.lint cargo-derivefmt-meli
- name: cargo-derivefmt tools
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f .gitea/Makefile.lint cargo-derivefmt-tools

View file

@ -0,0 +1,98 @@
# SPDX-License-Identifier: EUPL-1.2
name: Cargo manifest lints
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility -C debuginfo=0"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
pull_request:
paths:
- '.gitea/**'
- 'melib/Cargo.toml'
- 'meli/Cargo.toml'
- 'fuzz/Cargo.toml'
- 'tool/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
- '.cargo/config.toml'
jobs:
manifest_lint:
name: Run Cargo manifest etc lints
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, ]
include:
- build: linux-amd64
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y mandoc
- name: Cache rustup
id: cache-rustup
uses: actions/cache@v4
with:
path: |
~/.rustup/
~/.cargo/env
~/.cargo/config.toml
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: manifest_lints-workflow-${{ matrix.build }}-rustup
- id: rustup-setup
name: Install Rustup and toolchains
shell: bash
run: |
if ! command -v rustup &>/dev/null; then
curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail "https://sh.rustup.rs" | sh -s -- --default-toolchain none -y
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
rustup toolchain install --profile minimal --component "rustfmt" --target "${{ matrix.target }}" -- "${{ matrix.rust }}"
rustup component add rustfmt --toolchain ${{ matrix.rust }}-${{ matrix.target }}
rustup default ${{ matrix.rust }}
fi
- name: Source .cargo/env
shell: bash
run: |
source "${HOME}/.cargo/env"
echo "${CARGO_HOME:-$HOME/.cargo}/bin" >> $GITHUB_PATH
echo "CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" >> $GITHUB_ENV
- name: Setup Rust target
if: steps.cache-rustup.outputs.cache-hit != 'true'
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- name: Add manifest lint dependencies
if: steps.cache-rustup.outputs.cache-hit != 'true'
run: |
source "${HOME}/.cargo/env"
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
- name: cargo-sort
if: success() || failure()
run: |
source "${HOME}/.cargo/env"
make -f ./.gitea/Makefile.manifest-lint cargo-sort
- name: Check debian/changelog is up-to-date.
if: success() || failure()
run: |
make -f ./.gitea/Makefile.manifest-lint check-debian-changelog

View file

@ -1,115 +0,0 @@
name: Tests
env:
CARGO_INCREMENTAL: 0
CARGO_NET_RETRY: 10
CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
RUSTFLAGS: "-D warnings -W unreachable-pub -W rust-2021-compatibility"
RUSTUP_MAX_RETRIES: 10
RUST_BACKTRACE: short
on:
workflow_dispatch:
pull_request:
paths:
- '.gitea/**'
- 'melib/src/**'
- 'melib/Cargo.toml'
- 'meli/src/**'
- 'meli/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
push:
paths:
- '.gitea/**'
- 'melib/src/**'
- 'melib/Cargo.toml'
- 'meli/src/**'
- 'meli/Cargo.toml'
- 'Cargo.toml'
- 'Cargo.lock'
jobs:
test:
name: Test on ${{ matrix.build }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
build: [linux-amd64, ]
include:
- build: linux-amd64
os: ubuntu-latest
rust: stable
target: x86_64-unknown-linux-gnu
steps:
- uses: https://github.com/actions/checkout@v2
- id: os-deps
name: install OS dependencies
run: |
apt-get update
apt-get install -y libdbus-1-dev pkg-config mandoc libssl-dev
#- id: cache-rustup
# name: Cache Rust toolchain
# uses: https://github.com/actions/cache@v3
# with:
# path: ~/.rustup
# key: toolchain-${{ matrix.os }}-${{ matrix.rust }}
#- if: ${{ steps.cache-rustup.outputs.cache-hit != 'true' }}
- name: Install Rust ${{ matrix.rust }}
uses: https://github.com/actions-rs/toolchain@v1
with:
profile: minimal
toolchain: ${{ matrix.rust }}
components: clippy, rustfmt
target: ${{ matrix.target }}
override: true
- name: Configure cargo data directory
# After this point, all cargo registry and crate data is stored in
# $GITHUB_WORKSPACE/.cargo_home. This allows us to cache only the files
# that are needed during the build process. Additionally, this works
# around a bug in the 'cache' action that causes directories outside of
# the workspace dir to be saved/restored incorrectly.
run: echo "CARGO_HOME=$(pwd)/.cargo_home" >> $GITHUB_ENV
#- id: cache-cargo
# name: Cache cargo configuration and installations
# uses: https://github.com/actions/cache@v3
# with:
# path: ${{ env.CARGO_HOME }}
# key: cargo-${{ matrix.os }}-${{ matrix.rust }}
#- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
- name: Setup Rust target
run: |
mkdir -p "${{ env.CARGO_HOME }}"
cat << EOF > "${{ env.CARGO_HOME }}"/config.toml
[build]
target = "${{ matrix.target }}"
EOF
- if: ${{ steps.cache-cargo.outputs.cache-hit != 'true' }} && matrix.target
name: Add lint dependencies
run: |
cargo install --quiet --version 1.0.9 --target "${{ matrix.target }}" cargo-sort
cargo install --quiet --version 0.9.54 --target "${{ matrix.target }}" cargo-nextest
- name: cargo-check
run: |
cargo check --all-features --all --tests --examples --benches --bins
- name: Compile
if: success() || failure()
run: cargo test --all --no-fail-fast --all-features --no-run --locked
- name: cargo test
if: success() || failure() # always run even if other steps fail, except when cancelled <https://stackoverflow.com/questions/58858429/how-to-run-a-github-actions-step-even-if-the-previous-step-fails-while-still-f>
run: |
cargo nextest run --all --no-fail-fast --all-features --future-incompat-report -E 'not (test(smtp::test::test_smtp))'
#cargo test --all --no-fail-fast --all-features -- --nocapture --quiet
- name: cargo-sort
if: success() || failure()
run: |
cargo sort --check
- name: rustfmt
if: success() || failure()
run: |
cargo fmt --check --all
- name: clippy
if: success() || failure()
run: |
cargo clippy --no-deps --all-features --all --tests --examples --benches --bins

8
.mailmap Normal file
View file

@ -0,0 +1,8 @@
# Copyright (c) 2024 Manos Pitsidianakis <manos@pitsidianak.is>
# Licensed under the EUPL-1.2-or-later.
#
# You may obtain a copy of the Licence at:
# https://joinup.ec.europa.eu/software/page/eupl
#
# SPDX-License-Identifier: EUPL-1.2
Manos Pitsidianakis <manos@pitsidianak.is> <el13635@mail.ntua.gr>

91
BUILD.md Normal file
View file

@ -0,0 +1,91 @@
# Build `meli`
For a quick start, build and install locally:
```sh
PREFIX=~/.local make install
```
Available subcommands for `make` are listed with `make help`.
The Makefile *should* be POSIX portable and not require a specific `make` version.
`meli` requires rust version 1.70.0 or later and rust's package manager, Cargo.
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
Run `make install` to install the binary and man pages.
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
## Build features
Some functionality is held behind "feature gates", or compile-time flags.
Cargo features for `meli` are documented in its [`README.md`](./meli/README.md) file.
Cargo features for `melib` are documented in its [`README.md`](./melib/README.md) file.
The following list explains each feature's purpose:
- `gpgme` enables GPG support via `libgpgme` (on by default)
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
## Build Debian package (*deb*)
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
A `*.deb` package can be built with `make deb-dist`
## Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
In Debian-like systems, install the `libnotmuch5` packages.
`meli` detects the library's presence on runtime.
If it is not detected, you can use the `library_file_path` setting on your notmuch account to specify the absolute path of the library.
## Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system.
In Debian-like systems, install the `libgpgme11` package.
`meli` detects the library's presence on runtime.
## Building and running on Android with `termux`
This is not a supported or stable setup so caveat emptor.
At the time of writing this, Android is not a stable Rust target.
The packaged Rust from `termux` will be used.
The following steps should suffice to build and run `meli` on `termux`:
```console
$ pkg install rust perl make m4 man
$ cargo install meli # ensure .cargo/bin is in your PATH
```
Exporting `EDITOR` and `PAGER` might be useful.
## Development
Development builds can be built and/or run with
```
cargo build
cargo run
```
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
To trace network and protocol communications you can enable the following features:
- `imap-trace`
- `jmap-trace`
- `nntp-trace`
- `smtp-trace`

File diff suppressed because it is too large Load diff

1918
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,50 @@
# Development
Code style follows the `rustfmt.toml` file.
## Trace logs
Enable trace logs to `stderr` with:
```sh
export MELI_DEBUG_STDERR=yes
```
This means you will have to to redirect `stderr` to a file like `meli 2> trace.log`.
Tracing is opt-in by build features:
```sh
cargo build --features=debug-tracing,imap-trace,smtp-trace
```
## use `.git-blame-ignore-revs` file _optional_
Use this file to ignore formatting commits from `git-blame`.
It needs to be set up per project because `git-blame` will fail if it's missing.
```sh
git config blame.ignoreRevsFile .git-blame-ignore-revs
```
## Formatting with `rustfmt`
```sh
make fmt
```
## Linting with `clippy`
```sh
make lint
```
## Testing
```sh
make test
```
How to run specific tests:
```sh
@ -11,14 +54,14 @@ cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
## Profiling
```sh
perf record -g target/debug/bin
perf record -g target/debug/meli
perf script | stackcollapse-perf | rust-unmangle | flamegraph > perf.svg
```
## Running fuzz targets
Note: `cargo-fuzz` requires the nightly toolchain.
```sh
cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict
```
<!-- -->
<!-- ## Running fuzz targets -->
<!-- -->
<!-- Note: `cargo-fuzz` requires the nightly toolchain. -->
<!-- -->
<!-- ```sh -->
<!-- cargo +nightly fuzz run envelope_parse -- -dict=fuzz/envelope_tokens.dict -->
<!-- ``` -->

165
Makefile
View file

@ -19,13 +19,13 @@
.POSIX:
.SUFFIXES:
CARGO_TARGET_DIR ?= target
MIN_RUSTC ?= 1.65.0
CARGO_BIN ?= cargo
TAGREF_BIN ?= tagref
CARGO_ARGS ?=
RUSTFLAGS ?= -D warnings -W unreachable-pub -W rust-2021-compatibility
CARGO_SORT_BIN = cargo-sort
PRINTF = /usr/bin/printf
CARGO_HACK_BIN = cargo-hack
PRINTF := `command -v printf`
# Options
PREFIX ?= /usr/local
@ -35,13 +35,14 @@ MANDIR ?= ${EXPANDED_PREFIX}/share/man
# Installation parameters
DOCS_SUBDIR ?= meli/docs/
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
FEATURES ?= --features "${MELI_FEATURES}"
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5 meli.7
FEATURES != [ -z "$${MELI_FEATURES}" ] && ($(PRINTF) -- '--all-features') || ($(PRINTF) -- '--features %s' "$${MELI_FEATURES}")
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo -n $${ACCUM} | sed 's/^://'
VERSION != sed -n "s/^version\s*=\s*\"\(.*\)\"/\1/p" Cargo.toml
GIT_COMMIT != git show-ref -s --abbrev HEAD
DATE != date -I
MANPATHS != ACCUM="";for m in `manpath 2> /dev/null | tr ':' ' '`; do if [ -d "$${m}" ]; then REAL_PATH=`cd $${m} && pwd` ACCUM="$${ACCUM}:$${REAL_PATH}";fi;done;echo $${ACCUM}'\c' | sed 's/^://'
VERSION = `grep -m1 version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1`
MIN_RUSTC = `grep -m1 rust-version meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1`
GIT_COMMIT = `git show-ref -s --abbrev HEAD`
DATE = `date -I`
# Output parameters
BOLD ?= `[ -z $${TERM} ] && echo "" || tput bold`
@ -50,19 +51,21 @@ ANSI_RESET ?= `[ -z $${TERM} ] && echo "" || tput sgr0`
CARGO_COLOR ?= `[ -z $${NO_COLOR+x} ] && echo "" || echo "--color=never "`
RED ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 1) || echo ""`
GREEN ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 2) || echo ""`
YELLOW ?= `[ -z $${NO_COLOR+x} ] && ([ -z $${TERM} ] && echo "" || tput setaf 3) || echo ""`
.PHONY: meli
meli: check-deps
@echo ${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --release --bin meli
@${CARGO_BIN} build ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --release --bin meli
.PHONY: help
help:
@echo "For a quick start, build and install locally:\n ${BOLD}${GREEN}PREFIX=~/.local make install${ANSI_RESET}\n"
@echo "For a quick start, build and install locally:\n\n${BOLD}${GREEN}make PREFIX=~/.local install${ANSI_RESET}\n"
@echo "Available subcommands:"
@echo " - ${BOLD}meli${ANSI_RESET} (builds meli with optimizations in \$$CARGO_TARGET_DIR)"
@echo " - ${BOLD}install${ANSI_RESET} (installs binary in \$$BINDIR and documentation to \$$MANDIR)"
@echo " - ${BOLD}uninstall${ANSI_RESET}"
@echo "Secondary subcommands:"
@echo "\nSecondary subcommands:"
@echo " - ${BOLD}clean${ANSI_RESET} (cleans build artifacts)"
@echo " - ${BOLD}check-deps${ANSI_RESET} (checks dependencies)"
@echo " - ${BOLD}install-bin${ANSI_RESET} (installs binary to \$$BINDIR)"
@ -73,48 +76,65 @@ help:
@echo " - ${BOLD}deb-dist${ANSI_RESET} (builds debian package in the parent directory)"
@echo " - ${BOLD}distclean${ANSI_RESET} (cleans distribution build artifacts)"
@echo " - ${BOLD}build-rustdoc${ANSI_RESET} (builds rustdoc documentation for all packages in \$$CARGO_TARGET_DIR)"
@echo "\nENVIRONMENT variables of interest:"
@echo "* PREFIX = ${UNDERLINE}${EXPANDED_PREFIX}${ANSI_RESET}"
@echo -n "* MELI_FEATURES = ${UNDERLINE}"
@[ -z $${MELI_FEATURES+x} ] && echo -n "unset" || echo -n ${MELI_FEATURES}
@echo ${ANSI_RESET}
@echo "* BINDIR = ${UNDERLINE}${BINDIR}${ANSI_RESET}"
@echo "* MANDIR = ${UNDERLINE}${MANDIR}${ANSI_RESET}"
@echo -n "* MANPATH = ${UNDERLINE}"
@[ $${MANPATH+x} ] && echo -n $${MANPATH} || echo -n "unset"
@echo ${ANSI_RESET}
@echo ""
@echo "ENVIRONMENT variables of interest:"
@$(PRINTF) "* MELI_FEATURES "
@[ -z $${MELI_FEATURES+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${MELI_FEATURES}${ANSI_RESET}
@$(PRINTF) "* PREFIX "
@[ -z ${EXPANDED_PREFIX} ] && echo "unset" || echo "= ${UNDERLINE}"${EXPANDED_PREFIX}${ANSI_RESET}
@$(PRINTF) "* BINDIR = %s\n" "${UNDERLINE}${BINDIR}${ANSI_RESET}"
@$(PRINTF) "* MANDIR "
@[ -z ${MANDIR} ] && echo "unset" || echo "= ${UNDERLINE}"${MANDIR}${ANSI_RESET}
@$(PRINTF) "* MANPATH = "
@[ $${MANPATH+x} ] && echo ${UNDERLINE}$${MANPATH}${ANSI_RESET} || echo "unset"
@echo "* (cleaned) output of manpath(1) = ${UNDERLINE}${MANPATHS}${ANSI_RESET}"
@echo -n "* NO_MAN ${UNDERLINE}"
@[ $${NO_MAN+x} ] && echo -n "set" || echo -n "unset"
@echo ${ANSI_RESET}
@echo -n "* NO_COLOR ${UNDERLINE}"
@[ $${NO_COLOR+x} ] && echo -n "set" || echo -n "unset"
@echo ${ANSI_RESET}
@$(PRINTF) "* NO_MAN "
@[ $${NO_MAN+x} ] && echo "set" || echo "unset"
@$(PRINTF) "* NO_COLOR "
@([ $${NO_COLOR+x} ] && [ "$${NO_COLOR}" != "" ] && echo "set") || echo "unset"
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${ANSI_RESET}"
@echo "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
@$(PRINTF) "* CARGO_ARGS "
@([ -z "${CARGO_ARGS}" ] && echo "unset") || echo = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}
@$(PRINTF) "* RUSTFLAGS = "
@([ -z "${RUSTFLAGS}" ] && echo "unset") || echo = ${UNDERLINE}${RUSTFLAGS}${ANSI_RESET}
@$(PRINTF) "* AUTHOR (for deb-dist) "
@[ -z $${AUTHOR+x} ] && echo "unset" || echo "= ${UNDERLINE}"$${AUTHOR}${ANSI_RESET}
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
@echo "* VERSION = ${UNDERLINE}${VERSION}${ANSI_RESET}"
@echo "* GIT_COMMIT = ${UNDERLINE}${GIT_COMMIT}${ANSI_RESET}"
@echo "* CARGO_TARGET_DIR = ${CARGO_TARGET_DIR}"
@echo ""
@echo "Built-in/binary utilities"
@echo "* PRINTF = ${UNDERLINE}${PRINTF}${ANSI_RESET}"
.PHONY: check
check: check-tagrefs
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} check ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
.PHONY: fmt
fmt:
@$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
@OUT=$$($(CARGO_SORT_BIN) -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
$(CARGO_BIN) +nightly fmt --all || $(CARGO_BIN) fmt --all
@OUT=$$($(CARGO_SORT_BIN) melib -w 2>&1 && $(CARGO_SORT_BIN) meli -w 2>&1) || $(PRINTF) "WARN: %s cargo-sort failed or binary not found in PATH.\n" "$$OUT"
.PHONY: lint
lint:
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' $(CARGO_BIN) clippy --no-deps ${FEATURES} --all --tests --examples --benches --bins
.PHONY: test
test: test-docs
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --tests --examples --benches --bins
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --tests --examples --benches --bins
.PHONY: test-docs
test-docs:
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --doc
@echo RUSTFLAGS=\"'${RUSTFLAGS}'\" ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" ${FEATURES} --all --doc
@RUSTFLAGS='${RUSTFLAGS}' ${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" ${FEATURES} --all --doc
.PHONY: test-feature-permutations
test-feature-permutations:
$(CARGO_HACK_BIN) hack --feature-powerset
.PHONY: check-deps
check-deps:
@ -127,69 +147,75 @@ clean:
-rm -rf ./${CARGO_TARGET_DIR}/
.PHONY: distclean
distclean: clean
@rm -f meli-${VERSION}.tar.gz
distclean:
rm -f meli-${VERSION}.tar.gz
rm -rf .pc # rm debian stuff
.PHONY: uninstall
uninstall:
rm -f $(DESTDIR)${BINDIR}/meli
-rm $(DESTDIR)${MANDIR}/man1/meli.1.gz
-rm $(DESTDIR)${MANDIR}/man5/meli.conf.5.gz
-rm $(DESTDIR)${MANDIR}/man5/meli-themes.5.gz
for MANPAGE in ${MANPAGES}; do \
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
MANPAGEPATH="${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz"; \
rm -f "$${MANAGEPATH}"
; done
.PHONY: install-doc
install-doc:
@(if [ -z $${NO_MAN+x} ]; then \
mkdir -p $(DESTDIR)${MANDIR}/man1 ; \
mkdir -p $(DESTDIR)${MANDIR}/man5 ; \
echo " - ${BOLD}Installing manpages to ${ANSI_RESET}${DESTDIR}${MANDIR}:" ; \
for MANPAGE in ${MANPAGES}; do \
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
mkdir -p $(DESTDIR)${MANDIR}/man$${SECTION} ; \
MANPAGEPATH=${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz; \
echo " * installing $${MANPAGE} → ${GREEN}$${MANPAGEPATH}${ANSI_RESET}"; \
gzip -n < ${DOCS_SUBDIR}$${MANPAGE} > $${MANPAGEPATH} \
; done ; \
(case ":${MANPATHS}:" in \
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
*:${DESTDIR}${MANDIR}:*) echo "\c";; \
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${MANDIR} is not contained in your MANPATH variable or the output of \`manpath\` command.${ANSI_RESET} \`man\` might fail finding the installed manpages. Consider adding it if necessary.\nMANPATH variable / output of \`manpath\`: ${MANPATHS}" ;; \
esac) ; \
else echo "NO_MAN is defined, so no documentation is going to be installed." ; fi)
.PHONY: install-bin
install-bin: meli
@mkdir -p $(DESTDIR)${BINDIR}
mkdir -p $(DESTDIR)${BINDIR}
@echo " - ${BOLD}Installing binary to ${ANSI_RESET}${GREEN}${DESTDIR}${BINDIR}/meli${ANSI_RESET}"
@case ":${PATH}:" in \
*:${DESTDIR}${BINDIR}:*) echo -n "";; \
*:${DESTDIR}${BINDIR}:*) echo "\n";; \
*) echo "\n${RED}${BOLD}WARNING${ANSI_RESET}: ${UNDERLINE}Path ${DESTDIR}${BINDIR} is not contained in your PATH variable.${ANSI_RESET} Consider adding it if necessary.\nPATH variable: ${PATH}";; \
esac
@mkdir -p $(DESTDIR)${BINDIR}
@rm -f $(DESTDIR)${BINDIR}/meli
@cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
@chmod 755 $(DESTDIR)${BINDIR}/meli
mkdir -p $(DESTDIR)${BINDIR}
rm -f $(DESTDIR)${BINDIR}/meli
cp ./${CARGO_TARGET_DIR}/release/meli $(DESTDIR)${BINDIR}/meli
chmod 755 $(DESTDIR)${BINDIR}/meli
.PHONY: install
.NOTPARALLEL: yes
install: meli install-bin install-doc
@(if [ -z $${NO_MAN+x} ]; then \
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
$(PRINTF) "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
$(PRINTF) "\n or the tutorial in meli(7) (\`man 7 meli\`).\n" ;\
fi)
@echo " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli.delivery${ANSI_RESET}"
@echo " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker."
@$(PRINTF) " - Report bugs in the mailing list or git issue tracker ${UNDERLINE}https://git.meli-email.org${ANSI_RESET}\n"
@$(PRINTF) " - If you have a specific feature or workflow you want to use, you can post in the mailing list or git issue tracker.\n"
.PHONY: dist
dist:
@git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
git archive --format=tar.gz --prefix=meli-${VERSION}/ HEAD >meli-${VERSION}.tar.gz
@echo meli-${VERSION}.tar.gz
AUTHOR ?= grep -m1 authors meli/Cargo.toml | head -n1 | cut -d'"' -f 2 | head -n1
.PHONY: deb-dist
deb-dist:
@dpkg-buildpackage -b -rfakeroot -us -uc
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.deb
@$(PRINTF) "Override AUTHOR environment variable to set package metadata.\n"
dpkg-buildpackage -b -rfakeroot -us -uc --build-by="${AUTHOR}" --release-by="${AUTHOR}"
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_`dpkg --print-architecture`.deb
.PHONY: build-rustdoc
build-rustdoc:
@echo RUSTDOCFLAGS=\""--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}"\" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir=\""${CARGO_TARGET_DIR}"\" --all-features --no-deps --workspace --document-private-items --open
@RUSTDOCFLAGS="--crate-version ${VERSION}_${GIT_COMMIT}_${DATE}" ${CARGO_BIN} doc ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all-features --no-deps --workspace --document-private-items --open
.PHONY: check-tagrefs
@ -201,3 +227,36 @@ check-tagrefs:
else \
$(TAGREF_BIN);\
fi)
.PHONY: test-makefile
test-makefile:
@$(PRINTF) "Checking that current version is detected. "
@([ ! -z "${VERSION}" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\nVERSION env var is empty, check its definition.\n" 1>&2
@$(PRINTF) "Checking that 'date -I' works on this platform. "
@export DATEVAL=$$(printf "%s" ${DATE} | wc -c | tr -d "[:blank:]" 2>&1); ([ "$${DATEVAL}" = "10" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${RED}ERROR${ANSI_RESET}\n'date -I' does not produce a YYYY-MM-DD output on this platform.\n" 1>&2
@$(PRINTF) "Checking that the git commit SHA can be detected. "
@([ ! -z "$(GIT_COMMIT)" ] && $(PRINTF) "${GREEN}OK${ANSI_RESET}\n") || $(PRINTF) "${YELLOW}WARN${ANSI_RESET}\nGIT_COMMIT env var is empty.\n" 1>&2
# Checking if mdoc changes produce new lint warnings from mandoc(1) compared to HEAD version:
#
# example invocation: `mandoc_lint meli.1`
#
# with diff(1)
# ============
#function mandoc_lint () {
#diff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}
#
# with sdiff(1) (side by side)
# ============================
#
#function mandoc_lint () {
#sdiff <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}
#
# with delta(1)
# =============
#
#function mandoc_lint () {
#delta --side-by-side <(mandoc -T lint <(git show HEAD:./meli/docs/$1) 2> /dev/null | cut -d':' -f 3-) <(mandoc -T lint ./meli/docs/$1 2> /dev/null | cut -d':' -f 3-)
#}

225
README.md
View file

@ -1,27 +1,82 @@
# meli [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
# meli ![Established, created in 2017](https://img.shields.io/badge/Est.-2017-blue) ![Minimum Supported Rust Version](https://img.shields.io/badge/MSRV-1.70.0-blue) [![GitHub license](https://img.shields.io/github/license/meli/meli)](https://github.com/meli/meli/blob/master/COPYING) [![Crates.io](https://img.shields.io/crates/v/meli)](https://crates.io/crates/meli) [![IRC channel](https://img.shields.io/badge/irc.oftc.net-%23meli-blue)](ircs://irc.oftc.net:6697/%23meli)
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
* [mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC
* Main repository: <https://git.meli.delivery/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
* Official mirrors:
- <https://codeberg.org/epilys/meli>
Try an [old, outdated but online and interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
* `#meli` on OFTC IRC
* [Mailing lists](https://lists.meli-email.org/)
* Main repository <https://git.meli-email.org/meli/meli> Report bugs and/or feature requests in [meli's issue tracker](https://git.meli-email.org/meli/meli/issues "meli gitea issue tracker")<details><summary>Official git mirrors</summary>
- <https://codeberg.org/meli/meli>
- <https://github.com/meli/meli>
- <https://ayllu-forge.org/meli/meli>
- <https://gitlab.com/meli-project/meli>
</details>
**Table of contents**:
- [Install](#install)
- [Build](#build)
- [Quick start](#quick-start)
- [Supported E-mail backends](#supported-e-mail-backends)
- [E-mail submission backends](#e-mail-submission-backends)
- [Non-exhaustive list of features](#non-exhaustive-list-of-features)
- [HTML Rendering](#html-rendering)
- [Documentation](#documentation)
## Install
- Try an [old online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
- Pre-built binaries for [pkgsrc](https://pkgsrc.se/mail/meli) and [openbsd ports](https://openports.pl/path/mail/meli).
- `cargo install meli` or `cargo install --git https://git.meli.delivery/meli/meli.git meli`
- [Download and install pre-built debian package, static linux binary](https://github.com/meli/meli/releases/ "github releases for meli"), or
- Install with [Nix](https://search.nixos.org/packages?show=meli&query=meli&from=0&size=30&sort=relevance&channel=unstable#disabled "nixos package search results for 'meli'").
<a href="https://repology.org/project/meli/versions">
<img src="https://repology.org/badge/vertical-allrepos/meli.svg" alt="Packaging status table by repology.org" align="right">
</a>
- `cargo install meli` or `cargo install --git https://git.meli-email.org/meli/meli.git meli` [crates.io link](https://crates.io/crates/meli)
- Official Debian packages <https://packages.debian.org/trixie/meli>
- AUR (archlinux) <https://aur.archlinux.org/packages/meli>
- NetBSD with pkgsrc <https://pkgsrc.se/mail/meli>
- OpenBSD ports <https://openports.pl/path/mail/meli>
- macOS with MacPorts <https://ports.macports.org/port/meli/>
- Nix with Nixpkgs <https://search.nixos.org/packages?query=meli>
- [Pre-built debian package, static binaries](https://github.com/meli/meli/releases/ "github releases for meli") for <code>amd64</code>, <code>arm64</code> architectures
## Build
Run `make` or `cargo build --release --bin meli`.
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
### Cargo Compile-time Features
`meli` supports opting in and out of features at compile time with cargo features.
The contents of the `default` feature are:
```toml
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
```
A list of all the features and a description for each follows:
| Feature flag | Dependencies | Notes |
|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <a name="notmuch-feature">`notmuch`</a> | `maildir` feature | Provides the *notmuch* backend |
| <a name="jmap-feature">`jmap`</a> | `http` feature, `url` crate with `serde` feature | Provides the *JMAP* backend |
| <a name="smtp-feature">`smtp`</a> | `tls` feature | Integrated async *SMTP* client |
| <a name="sqlite3-feature">`sqlite3`</a> | `rusqlite` crate with `bundled-full` feature | Used in caches |
| <a name="sqlite3-static-feature">`sqlite3-static`</a> | `rusqlite` crate with `bundled-full` feature | Same as `sqlite3` feature but provided for consistency and in case `sqlite3` feature stops bundling libsqlite3 statically in the future. |
| <a name="smtp-trace-feature">`smtp-trace`</a> | `smtp` feature | Connection trace logs on the `trace` logging level |
| <a name="gpgme-feature">`gpgme`</a> | | *GPG* use by dynamically loading `libgpgme.so` |
| <a name="tls-static-feature">`tls-static`</a> | `native-tls` crate with `vendored` feature | Links with `OpenSSL` statically where it's used |
| <a name="http-static-feature">`http-static`</a> | `isahc` crate with `static-curl` feature | Links with `curl` statically |
| <a name="dbus-notifications-feature">`dbus-notifications`</a> | `notify-rust` dependency | Uses DBus notifications |
| <a name="dbus-static-feature">`dbus-static`</a> | `notify-rust` dependency and enableds its `d_vendored` feature | Includes the dbus library statically. |
| <a name="cli-docs-feature">`cli-docs`</a> | `flate2` dependency | Includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` |
| <a name="libz-static-feature">`libz-static`</a> | `libz-sys` dependency and enables its `static` feature | Allows for the transitive dependency libz (from `curl`) to be linked statically. |
| <a name="static-feature">`static`</a> | enables `tls-static`, `http-static`, `sqlite3-static`, `dbus-static`, `libz-static` features | |
## Quick start
<table>
<tr><td>
```shell
```sh
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
$ meli create-config
# Edit configuration in ${EDITOR} or ${VISUAL}:
@ -30,15 +85,21 @@ $ meli edit-config
$ meli install-man
# Ready to go.
$ meli
# You can read any manual page with the CLI subcommand `man`:
$ meli man meli.7
# See help output for all options and subcommands.
$ meli --help
```
</td><td>
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation. Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation"). `meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Examples for configuration file settings can be found in `meli.conf.examples(5)`
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`.
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
@ -46,26 +107,22 @@ You can run meli with arbitrary configuration files by setting the `${MELI_CONFI
MELI_CONFIG=./test_config cargo run
```
</td></tr>
</table>
See [`meli(7)`](./meli/docs/meli.7) for an extensive tutorial and [`meli.conf(5)`](./meli/docs/meli.conf.5) for all configuration values.
| | | |
:---:|:---:|:---:
![Main view screenshot](./meli/docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./meli/docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./meli/docs/screenshots/compose.webp "composing view screenshot")
Main view | Compact main view | Compose with embed terminal editor
| Main view | Compact main view | Compose with embed terminal editor |
|-----------|-------------------|------------------------------------|
| ![Main view screenshot](./meli/docs/screenshots/main.webp "mail meli view screenshot") | ![Compact main view screenshot](./meli/docs/screenshots/compact.webp "compact main view screenshot") | ![Compose with embed terminal editor screenshot](./meli/docs/screenshots/compose.webp "composing view screenshot") |
### Supported E-mail backends
| Protocol | Support |
|:------------:|:----------------|
| IMAP | full |
| Maildir | full |
| notmuch | full[^0] |
| mbox | read-only |
| JMAP | functional |
| NNTP / Usenet| functional |
| Protocol | Support |
|---------------|------------|
| IMAP | full |
| Maildir | full |
| notmuch | full[^0] |
| mbox | read-only |
| JMAP | functional |
| NNTP / Usenet | functional |
[^0]: there's no support for searching through all email directly, you'd have to
create a mailbox with a notmuch query that returns everything and search
@ -100,92 +157,32 @@ Main view | Compact main view | Compose with embed terminal editor
- GPG signing, encryption, signing + encryption
- GPG signature verification
## Documentation
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, i.e.:
```sh
MELI_CONFIG=./test_config cargo run
```
## Build
For a quick start, build and install locally:
```sh
PREFIX=~/.local make install
```
Available subcommands for `make` are listed with `make help`.
The Makefile *should* be POSIX portable and not require a specific `make` version.
`meli` requires rust 1.65 and rust's package manager, Cargo.
Information on how to get it on your system can be found here: <https://doc.rust-lang.org/cargo/getting-started/installation.html>
With Cargo available, the project can be built with `make` and the resulting binary will then be found under `target/release/meli`.
Run `make install` to install the binary and man pages.
This requires root, so I suggest you override the default paths and install it in your `$HOME`: `make PREFIX=${HOME}/.local install`.
You can build and run `meli` with one command: `cargo run --release`.
### Build features
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
- `gpgme` enables GPG support via `libgpgme` (on by default)
- `dbus-notifications` enables showing notifications using `dbus` (on by default)
- `notmuch` provides support for using a notmuch database as a mail backend (on by default)
- `jmap` provides support for connecting to a jmap server and use it as a mail backend (on by default)
- `sqlite3` provides support for builting fast search indexes in local sqlite3 databases (on by default)
- `cli-docs` includes the manpage documentation compiled by either `mandoc` or `man` binary to plain text in `meli`'s command line. Embedded documentation can be viewed with the subcommand `meli man [PAGE]` (on by default).
- `regexp` provides experimental support for theming some e-mail fields based
on regular expressions.
It uses the `pcre2` library.
Since it's actual use in the code is very limited, it is not recommended to use this (off by default).
- `static` and `*-static` bundle C libraries in dependencies so that you don't need them installed in your system (on by default).
### Build Debian package (*deb*)
Building with Debian's packaged cargo might require the installation of these two packages: `librust-openssl-sys-dev librust-libdbus-sys-dev`
A `*.deb` package can be built with `make deb-dist`
### Using notmuch
To use the optional notmuch backend feature, you must have `libnotmuch5` installed in your system.
In Debian-like systems, install the `libnotmuch5` packages.
`meli` detects the library's presence on runtime.
### Using GPG
To use the optional gpg feature, you must have `libgpgme` installed in your system.
In Debian-like systems, install the `libgpgme11` package.
`meli` detects the library's presence on runtime.
### HTML Rendering
## HTML Rendering
HTML rendering is achieved using [w3m](https://github.com/tats/w3m) by default.
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./meli/docs/meli.conf.5)).
# Development
Development builds can be built and/or run with
## Documentation
```
cargo build
cargo run
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./meli/docs/meli.7).
See also the [Quickstart tutorial](https://meli-email.org/documentation.html#quick-start) online.
After installing `meli`, see `meli(1)`, `meli.conf(5)`, `meli(7)` and `meli-themes(5)` for documentation.
Sample configuration and theme files can be found in the `meli/docs/samples/` subdirectory.
Manual pages are also [hosted online](https://meli-email.org/documentation.html "meli documentation").
`meli` by default looks for a configuration file in this location: `${XDG_CONFIG_HOME}/meli/config.toml`
You can run meli with arbitrary configuration files by setting the `${MELI_CONFIG}` environment variable to their locations, or use the `[-c, --config]` argument:
```sh
MELI_CONFIG=./test_config meli
```
There is a debug/tracing log feature that can be enabled by using the flag `--feature debug-tracing` after uncommenting the features in `Cargo.toml`.
The logs are printed in stderr when the env var `MELI_DEBUG_STDERR` is defined, thus you can run `meli` with a redirection (i.e `2> log`).
or
Code style follows the `rustfmt.toml` file.
```sh
meli -c ./test_config
```

166
cliff.toml Normal file
View file

@ -0,0 +1,166 @@
# configuration for https://github.com/orhun/git-cliff
[remote.gitea]
owner = "meli"
repo = "meli"
[changelog]
# changelog header
header = """
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
# note that the - before / after the % controls whether whitespace is rendered between each line.
# Getting this right so that the markdown renders with the correct number of lines between headings
# code fences and list items is pretty finicky. Note also that the 4 backticks in the commit macro
# is intentional as this escapes any backticks in the commit body.
body = """
{% if not version %}
## Unreleased
{% else %}
## [{{ version }}]({{ "https://git.meli-email.org/meli/meli/releases/tag/" ~ version }}) - {{ timestamp | date(format="%Y-%m-%d") }}
{% endif %}{% if get_env(name = "FRIENDS", default = "") != "" %}
Contributors in alphabetical order:
{{ get_env(name = "FRIENDS") }}
{%- endif -%}{% if gitea and gitea.contributors %}{% for contributor in gitea.contributors | filter(attribute="is_first_time", value=true) %}
* [@{{ contributor.username }}](https://git.meli-email.org/{{ contributor.username }}) made their first contribution in [#{{ contributor.pr_number }}]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ contributor.pr_number }})
{%- endfor -%}{%- endif -%}{% macro commit(commit) -%}
- [**`{{ commit.id | truncate(length=8, end="") }}`**]({{ "https://git.meli-email.org/meli/meli/commit/" ~ commit.id }}) {% if commit.scope %}*({{commit.scope | lower }})* {% endif %}`{{ commit.message | split(pat="\n")| first }}`{% if commit.remote and commit.remote.pr_number and commit.remote.pr_title %} in PR [`#{{ commit.remote.pr_number }}` "{{ commit.remote.pr_title }}"]({{ "https://git.meli-email.org/meli/meli/pulls/" ~ commit.remote.pr_number }}){%- endif -%}{% endmacro -%}
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits | filter(attribute="scope") | sort(attribute="scope") %}
{{ self::commit(commit=commit) }}
{%- endfor -%}
{% for commit in commits %}
{%- if not commit.scope %}
{{ self::commit(commit=commit) }}
{%- endif -%}
{%- endfor -%}
{%- endfor %}
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff <https://git-cliff.org> -->
"""
[git]
# don't parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = false
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://git.meli-email.org/meli/meli/issues/${2}))" }, # Replace the issue number with the link
{ pattern = " +", replace = " "}, # Replace multiple spaces with a single space
{ pattern = "`[^`]+`", replace_command = "pandoc -f commonmark -t plain" },
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^chore\\(changelog\\)", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^CHANGELOG", skip = true },
{ message = "(?i)^revert", group = "<!-- 11 -->Reverted Commits" },
{ message = "^.github", group = "<!-- 10 -->Continuous Integration" },
{ message = "^.gitea", group = "<!-- 10 -->Continuous Integration" },
{ message = "(?i)^ci", group = "<!-- 10 -->Continuous Integration" },
{ message = "^build", group = "<!-- 09 -->Build" },
{ body = ".*security", group = "<!-- 08 -->Security" },
{ message = "^scripts", group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = '(?i)^chore', group = "<!-- 07 -->Miscellaneous Tasks" },
{ message = "^debian", group = "<!-- 06 -->Packaging" },
{ message = "^test", group = "<!-- 06 -->Testing" },
{ message = "^style", group = "<!-- 05 -->Styling" },
{ message = "^perf", group = "<!-- 04 -->Performance" },
{ message = "(?i)readme", group = "<!-- 03 -->Documentation" },
{ message = "(?i)anpage", group = "<!-- 03 -->Documentation" },
{ message = "(?i)anual", group = "<!-- 03 -->Documentation" },
{ message = "meli.[17]", group = "<!-- 03 -->Documentation" },
{ message = "meli.conf.5", group = "<!-- 03 -->Documentation" },
{ message = "meli-themes.5", group = "<!-- 03 -->Documentation" },
{ message = "README.md", group = "<!-- 03 -->Documentation" },
{ message = "BUILD.md", group = "<!-- 03 -->Documentation" },
{ message = "DEVELOPMENT.md", group = "<!-- 03 -->Documentation" },
{ message = "^doc", group = "<!-- 03 -->Documentation" },
{ message = "^[^.]*.rs:", group = "<!-- 02 -->Refactoring" },
{ message = '(?i)^refactor\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)lints?\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)move\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)replace\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)remove\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)refactor\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)rename\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)formatting\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)cleanups?\b', group = "<!-- 02 -->Refactoring" },
{ message = '(?i)fix\b', group = "<!-- 01 -->Bug Fixes" },
{ message = '(?i)fixups?\b', group = "<!-- 01 -->Bug Fixes" },
{ message = "(?i)implement", group = "<!-- 00 -->Added" },
{ message = '(?i)add\b', group = "<!-- 00 -->Added" },
{ message = '(?i)^update\b', group = "<!-- 02 -->Changes" },
{ message = '(?i)\bdependency\b', group = "<!-- 02 -->Changes" },
{ message = "^feat", group = "<!-- 00 -->Added" },
{ message = '(?i)retry\b', group = "<!-- 02 -->Changes" },
{ message = "^conf", group = "<!-- 02 -->Changes" },
{ message = "^contacts?", group = "<!-- 02 -->Changes" },
{ message = "^compos[ie]?", group = "<!-- 02 -->Changes" },
{ message = "^types", group = "<!-- 02 -->Changes" },
{ message = '(?i)^use', group = "<!-- 02 -->Changes" },
{ message = "^terminal", group = "<!-- 02 -->Changes" },
{ message = "^listing", group = "<!-- 02 -->Changes" },
{ message = "^mail", group = "<!-- 02 -->Changes" },
{ message = "^utilities", group = "<!-- 02 -->Changes" },
{ message = "^view", group = "<!-- 02 -->Changes" },
{ message = "^mail/view", group = "<!-- 02 -->Changes" },
{ message = "^backends?", group = "<!-- 02 -->Changes" },
{ message = "^commands?", group = "<!-- 02 -->Changes" },
{ message = "^actions?", group = "<!-- 02 -->Changes" },
{ message = "^log", group = "<!-- 02 -->Changes" },
{ message = "^pgp", group = "<!-- 02 -->Changes" },
{ message = "^gpgme", group = "<!-- 02 -->Changes" },
{ message = "^manage", group = "<!-- 02 -->Changes" },
{ message = "^smtp", group = "<!-- 02 -->Changes" },
{ message = "^mbox", group = "<!-- 02 -->Changes" },
{ message = "^jmap", group = "<!-- 02 -->Changes" },
{ message = "^imap", group = "<!-- 02 -->Changes" },
{ message = "^nntp", group = "<!-- 02 -->Changes" },
{ message = "^notmuch", group = "<!-- 02 -->Changes" },
{ message = "^melib", group = "<!-- 02 -->Changes" },
{ message = "^meli", group = "<!-- 02 -->Changes" },
{ message = "^accounts", group = "<!-- 02 -->Changes" },
{ message = "^embedded", group = "<!-- 02 -->Changes" },
{ message = "^jobs", group = "<!-- 02 -->Changes" },
{ message = ".*", group = "<!-- 07 -->Miscellaneous Tasks" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# glob pattern for matching git tags
tag_pattern = "v[0-9]+|alpha-[0-9]+"
# regex for ignoring tags
ignore_tags = "v[^-]+-rc[.]?[0-9]+"
# regex for skipping tags
#skip_tags = "alpha"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"

69
codemeta.json Normal file
View file

@ -0,0 +1,69 @@
{
"@context": ["https://doi.org/10.5063/schema/codemeta-2.0", "http://schema.org/"],
"@type": "SoftwareSourceCode",
"applicationCategory": "E-mail client",
"author": [
{
"@id": "https://pitsidianak.is/",
"@type": "Person",
"name": "epilys",
"email": "manos@pitsidianak.is",
"familyName": "Pitsidianakis",
"givenName": "Manos",
"url": "https://pitsidianak.is/"
}
],
"codeRepository": "https://git.meli-email.org/meli/meli.git",
"dateCreated": "2016-04-25",
"dateModified": "2024-11-27",
"datePublished": "2017-07-23",
"description": "BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).",
"downloadUrl": "https://git.meli-email.org/meli/meli/archive/v0.8.10.tar.gz",
"identifier": "https://meli-email.org/",
"isPartOf": "https://meli-email.org/",
"keywords": [
"e-mail",
"email",
"mail",
"terminal user interface",
"client",
"mua",
"mail user agent",
"smtp",
"imap",
"jmap",
"mbox",
"maildir",
"nntp"
],
"license": [
"https://spdx.org/licenses/EUPL-1.2",
"https://spdx.org/licenses/GPL-3.0-or-later"
],
"name": "meli",
"operatingSystem": [
"Linux",
"macOS",
"OpenBSD",
"NetBSD"
],
"programmingLanguage": "Rust",
"relatedLink": [
"https://lists.meli-email.org/",
"https://codeberg.org/meli/meli",
"https://github.com/meli/meli",
"https://gitlab.com/meli-project/meli",
"https://crates.io/crates/meli",
"https://packages.debian.org/trixie/meli",
"https://pkgsrc.se/mail/meli",
"https://openports.pl/path/mail/meli",
"https://ports.macports.org/port/meli/",
"https://search.nixos.org/packages?query=meli"
],
"version": "0.8.10",
"contIntegration": "https://git.meli-email.org/meli/meli/actions",
"developmentStatus": "active",
"issueTracker": "https://git.meli-email.org/meli/meli/issues",
"readme": "https://git.meli-email.org/meli/meli/raw/tag/v0.8.10/README.md",
"buildInstructions": "https://git.meli-email.org/meli/meli/raw/tag/v0.8.10/BUILD.md"
}

50
contrib/README.md Normal file
View file

@ -0,0 +1,50 @@
<!-- SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later -->
# Useful scripts and files for use with `meli`
This directory includes various useful scripts and files that are contributed
by the community and not actively maintained or supported.
If you believe something in this directory needs updates to work with the
current version of `meli` or there are bugs that need fixing, please file an
issue on our issue tracker!
## Connecting to a Gmail account with OAUTH2
The script [`./oauth2.py`](./oauth2.py) is a helper script to authenticate to a Gmail account using IMAP OAUTH2 tokens.
See [`meli.conf(5)`](../meli/docs/meli.conf.5) for documentation.
If the script does not work and you're certain it's because it needs changes to
work with Google's servers and not a user error on your part, please file a bug
on our issue tracker!
## Using `meli` for `mailto:` links
To use `meli` to open `mailto:` links from your browser place the [`mailto-meli`](./mailto-meli) and [`mailto-meli-expect`](./mailto-meli-expect) scripts into `/usr/bin`
(or `.local/bin`, and adjust the path in the script accordingly).
Ensure all scripts are executable by your user account, if not set the permissions accordingly:
```sh
chmod u+x /path/to/mailto-meli
```
and
```sh
chmod u+x /path/to/mailto-meli-expect
```
Then set `mailto-meli` as program to open `mailto` links
in your browser.
E.g. in Firefox this can be done under "Settings" (`about:preferences`) which you can access from the menu button or `Edit -> Settings`.
```text
General -> Applications -> Content-Type: mailto.
```
You can test that it works by clicking the system menu entry `File -> Email link...`.
_NOTE_: that you need to have the [`expect`](https://en.wikipedia.org/wiki/Expect) binary installed for this to work.
`expect` is a scripting language used for interactive with interactive terminal applications like `meli`.

21
contrib/mailto-meli Executable file
View file

@ -0,0 +1,21 @@
#!/usr/bin/env sh
#
# mailto-meli -- wrapper to use meli as mailto handler
# To use meli as mailto: handler point your browser to use this as application for opening
# mailto: links.
# Note: This assumes that x-terminal-emulator supports the "-e" flag for passing along arguments.
# Copyright: 2024 Matthias Geiger <werdahias@debian.org>
# SPDX-License-Identifier: GPL-3.0-or-later
# Check if mailto-meli and expect are present
if ! command -v mailto-meli > /dev/null 2>&1
then echo "mailto-meli not found" && exit 1
else
if ! command -v expect > /dev/null 2>&1
then echo "expect not found" && exit 1
fi
fi
exec x-terminal-emulator -e mailto-meli-expect "$@"

18
contrib/mailto-meli-expect Executable file
View file

@ -0,0 +1,18 @@
#!/usr/bin/env -S expect -f
# Copyright 2024 Manos Pitsidianakis
#
# SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
# Trap window resize signal
trap {
set rows [stty rows]
set cols [stty columns]
stty rows $rows columns $cols < $spawn_out(slave,name)
} WINCH
# send the input with human-like delay:
set send_human {.001 .003 0.01 .005 .005}
spawn meli
send -h ":mailto "
send -h [lindex $argv 0]
send -h "\n"
interact

872
debian/changelog vendored
View file

@ -1,6 +1,876 @@
meli (0.8.10-1) bookworm; urgency=low
Highlights:
===========
- added pipe-attachment command
- added sample scripts for using meli as a mailto scheme handler in
contrib/
- fixed GPG encryption with libgpgme
Contributors in alphabetical order:
===================================
- Manos Pitsidianakis
- Matthias Geiger
Added
=====
- 5e77821f mail/view: add pipe-attachment command in PR #540
"mail/view: add pipe-attachment command"
- fa896f6b contrib: add mailto: scheme handler scripts
- 00ce9660
melib/backends: add as_any/as_any_mut methods to BackendMailbox
- fd243fa5 maildir: add mailbox creation tests
- de65eec3 meli/accounts: add mailbox_by_path() tests in PR #535
"Rework maildir mailbox path logic, add tests"
- 6b363601 melib/gpgme: impl Display for gpgme::Key
Bug Fixes
=========
- 60c90d75 melib/attachments: ensure MIME boundary prefixed with CRLF
- 3433c5c3 compose/pgp: rewrite key selection logic in PR #541 "More
gpgme/PGP fixes again"
- 12de82e7 melib/conf: fix mutt_alias_file not being validated in PR
#550 "Remove sealed_test dependency"
- c8e055a7 Fix version migrations being triggered backwards in PR #557
"Fix version migrations being triggered backwards"
- efab99fd
terminal: check for NO_COLOR env var without unicode validation
- 36a63e88 melib/maildir: rewrite create_mailbox()
- fcab855f view: ensure envelope headers are always populated in PR
#538 "view: ensure envelope headers are always populated"
- 84564f44 mailcap: don't drop File before opening it in PR #552
"mailcap: don't drop File before opening it"
Changes
=======
- ed85da51 Remove sealed_test dependency
Refactoring
===========
- 03df2ac1 meli/utilities: add print utilities for tests
- 18e9d5c1 conf.rs: impl From<melib::AccountSettings> for AccountConf
- 1f2fec19 Fix 1.83.0 lints in PR #536 "CI: Add action to check for
DCO signoffs in PRs"
- 192ecea2 compose/gpg.rs: Fix msrv regression
Documentation
=============
- 4a61a4b8 melib: include README.md as preamble of crate rustdocs
- 80e53471 BUILD.md: move melib specific stuff to melib/README.md
- 91a17ece melib/README.md: mention sqlite3-static feature
- b77a691b meli/README.md: Add cargo features section in PR #549
"Document cargo features in READMEs"
- 91dc271d contrib: add a README.md file
- 2e900be6 contrib/README.md: add section about oauth2.py
- 07812d2c contrib/README.md: elaborate a bit about mailto in PR #545
"Add external mailto: handler support via scripts in contrib"
- e784e8d2 scripts: add markdown_doc_lints.py
Continuous Integration
======================
- 77629851 CI: Add action to check for DCO signoffs in PRs
- f944ebed CI: Add error msg when cargo-derivefmt check fails
- d49344f9 CI: Move MSRV checks from manifest to lints in PR #553
"ci-workflow-fixes"
- ece6bfc2 CI: non-zero exit if cargo-derivefmt-* targets fail
- 2257b91b CI: add actions/cache steps in PR #554 "CI: add
actions/cache steps"
- a1c9524f CI: fix check_dco.sh not working with other repos in PR
#555 "CI: fix check_dco.sh not working with other repos"
-- Manos Pitsidianakis <manos@pitsidianak.is> Fri, 06 Dec 2024 07:03:58 +0200
meli (0.8.9-1) bookworm; urgency=low
This is mostly a fixups release.
Added
=====
- cf16bf65 meli/sqlite3: add tests for reindexing
- a389772d accounts: suggest tips on mailbox_by_path error
Bug Fixes
=========
- 25f0a3f8 conf/terminal: fix serde of ProgressSpinnerSequence
- c375b48e terminal: fix Synchronized Output response parsed as input
in PR #523 "terminal: fix Synchronized Output response parsed as
input"
- b7e215f9
melib/utils: fix test_fd_locks() on platforms without OFD support in
PR #524 "melib/utils: fix test_fd_locks() on platforms without OFD
support"
- 25c32a6b meli/docs/meli.conf.examples.5: fix .Dt macro arguments
- 18ae5848 meli: fix reindex of previously indexed account with sqlite3
backend
- 13e917d9 Fix some compilation errors with cfg feature attrs in PR #531
"accounts: suggest tips on mailbox_by_path error"
- 8c176d38 contacts/editor: fix crash on saving contact in PR #532
"contacts/editor: fix crash on saving contact"
- fb5a88c2
melib/collection: ensure mailbox exists when inserting new envelopes
in PR #529 "Small account stuff fixes"
Changes
=======
- 7f8f1cf6 melib/gpgme bindings renewal in PR #533 "melib/gpgme
bindings renewal"
- 9b7825bc Update futures-util dep, remove stderrlog dep
- 4be69360 Remove obsolete "encoding" dependency in PR #530
"Remove/update obsolete dependencies"
Refactoring
===========
- 5af6e059 meli/accounts: use Arc<str> for account name
- 567270e1 melib: use Vec instead of SmallVec for search results
- 2bd8d7ba
conf/tests.rs: Rename test functions to follow path convention
Documentation
=============
- 97242482 meli/docs: add meli.conf.examples to CLI and tests
- 0f096338 README.md: Update ways to install, add gitlab mirror link
in PR #528 "Integrate meli.conf.examples.5 into CLI and build, also
update README with installation instructions"
Continuous Integration
======================
- 630df308 CI: Add arm64 runners in job matrices in PR #527 "CI: Add
arm64 runners in job matrices"
- 49ecbb56 CI: .gitea/Makefile.lint: check if nightly exists
-- Manos Pitsidianakis <manos@pitsidianak.is> Wed, 27 Nov 2024 16:16:06 +0200
meli (0.8.8-1) bookworm; urgency=low
WARNING: This release contains a breaking change in the configuration
file: a global composing option is not required anymore. Now, composing
options are per account.
Added
=====
- f3d59ebf accounts: add force: bool arg to load()
- 33836a32 melib/error: add WrapResultIntoError helper trait
- 3216324c melib/mbox: impl FromStr for MboxFormat
- 94f345d7 Implement mailbox renaming command
- 8d45ecc1 melib/error: add related_path field
- bf3a4c5d error: add ErrorChainDisplay struct for better output
- 6be5fd26
themes: add inheritance, and use themes when initializing grids
- 0ee7fc4d Print clickable path links with subcommands
- aed7a60f samples: add ibm-modern theme in PR #469 "conf-refactor"
- 4bbf446b utils: add unix file locks module
- 6fbf569f search: add Message-ID, and other header search support
- 26d33ce5 address: add separator argument to display_slice()
- 32e3be8b
sqlite3: add optional directory field in DatabaseDescription
- dbbb1529 Add missing ComponentUnrealize handlers
- 87d2cec9 Add sealed_test dependency
- 604ae111 Impl From<&[u8]> for u64-based hash newtypes
- 8205c7f5 melib: add JsContact module in PR #479 "view-filters"
- 2af5c8b6 terminal: add QuerySynchronizedOutputSupport WIP
- 5c4faea5 Add transpose shortcut and tests for text field
- e9b87b2e melib/maildr: add rename_regex config option
- 8f0e1d66 Add human-readable identifiers in temp draft files
- 601e3711 Add vCard exports
- 719e2eb2 listing: add customizable view divider like sidebar's in PR
#485 "listing: add customizable view divider like sidebar's"
- ba3ad8ed listing: always show mail_view_divider in PR #486 "listing:
always show mail_view_divider"
- 46b2c3b1 Add listing.thread_layout config flag in PR #487 "Add
listing.thread_layout config flag"
- aaea3a5a nntp: add timeout conf flag
- d4636bcc nntp: interpret IMPLEMENTATION cap as metadata
- 5f120309 nntp: add select_group_by_name() method
- 9a9cd03d nntp: add NntpType::article_message_id() method
- 7cfcbb7a Add patch_retrieve module in PR #489 "Add patch_retrieve
module"
- c82341f3 File: try trimming filename if ENAMETOOLONG
- 23395491 compose/pgp: add encrypt_for_self flag
- 0b6988b7 gpgme: add always trust flag to encrypt op
- be3b3ef8 melib/utils: add fnmatch(3) interface
- 32f7e50f Add version migration support
- a6c7621c jscontact: add {created,updated} fields
- 39592ad0 jmap: implement changing mailbox subscription
- ca7eb792 jmap: Implement deleting email
- b8e841bb jmap: implement mailbox deletion
- 77e7c3df Add support for signatures in PR #500 "Add support for
signatures"
- dba5b68b components: add prelude module
- f656aff0 composer: add discard-draft command
- 789a88b2 shortcuts: add select_motion equivalent to select_entry
- cb2dd5de listing/threaded: impl missing select functionality in PR
#514 "listing/threaded: impl missing filter functionality"
- c1901c96
melib/email/compose: add Content-Type header for utf8 text plain attachments
- 0e77bd5b
melib/email/compose/tests: add multipart mixed attachment test in PR
#515 "Fix incorrect multipart/mixed rendering when sending text with
attachments under certain circumstances"
- 7b1be139 melib: make mbox backend build by default
- 7ff1db14 manage-mailboxes: add delete option in PR #520
"manage-mailboxes: add delete option"
Bug Fixes
=========
- 6b05279a Update time dep to fix 1.80.0 breakage
- 2084ce93 Fix invalid cfg feature combinations for macos in PR #471
"Fix invalid cfg feature combinations for macos"
- 4707ec9f text/line_break: fix ReflowState::{No,All} break
- 86e25bc0 sqlite: fix database reset sequence
- 4d4e189c imap: code style fixups
- 335cca88 listing: fix highlight_self flag off by one error in PR
#477 "listing: fix highlight_self flag off by one error"
- 80915832 mailto: rewrite parsing in PR #480 "mailto-rewrite"
- 65b32e77 subcommands: Fix wrong help info in imap-shell prompt
- d0c81749 conf::data_types: minor style and error msg fixups
- 7dbee81d view: fix nested filter jobs never being completed
- f78884ce melib/nntp: fix an ancient FIXME
- e0cfe8e4 Fix compilation for 32-bit architectures in PR #492 "Fix
compilation for 32-bit architectures"
- 1b708a99 melib: attempt FromSql from Blob for u64 hash in PR #506
"melib: attempt FromSql from Blob for u64 hash"
- 6c315580 compose: fix add-attachment-file-picker
- c6e9e424 listing/threaded: impl missing filter functionality
- e7a164de Configure some gpgme stuff under gpgme feature
Changes
=======
- 8e300c46 melib/jmap: call req text(). asap
- 374ea8ba accounts: extract tests to tests.rs file
- 7020cd66 meli: derive PartialEq/Eq for some types
- 69065859 accounts: split mailbox to enum out of JobRequest
- 14f2d911 melib/backends: change RefreshEvent field decl order
- 56b1bf28 meli/accounts: batch process refresh events
- 6513c188 melib/imap: on sync only update exists/unseen if loaded
- a8dad317 melib/imap: renamed cache module to sync
- 9e9c04a3 Update indexmap dep to 2.3.0
- 2b3828d8 Update futures dependency to 0.3.30
- 84812941
melib/jmap: do not serialize server-set fields in Set create
- eda6620c jmap: detect supported Auth schemes on connect in PR #467
"jmap: detect supported Auth schemes on connect"
- 35f12b15 embedded: prevent double-close of pty fd in PR #468
"embedded: prevent double-close of pty fd"
- 0bed37b5 melib: use IndexMap in conf fields
- f3ad824d meli: use itoa to format offset indices in listings
- 1cfb0b15 Update nix dependency to 0.29.0
- 9c1b4424 jobs: make cancel flag an AtomicBool
- f06a9072 jmap: fetch mailbox with receivedAt descending sort
- 53b0d035 accounts: cancel any previous mailbox fetches
- 60833ee5 accounts: make mailbox available as soon as possible
- 28f45805 mail/view: try cancel env fetch on Drop
- 2bb9b20d mail/view: do not highlight reply subjects in thread
- a4f344b3 Use create_new to avoid overwriting files
- d6197e8b listing: clear count modifier on Home/End
- b798ca4a imap: return cached response in {select,examine}_mailbox()
- 151fcebe imap: use BTreeMap for message sequence number store
- e48fcc33 imap/protocol_parser: also populate other_headers
- 1e11c29c imap: resync cache first when fetching a mailbox
- 1779ad5d imap: interpret empty server response as BYE
- 2d320688 mail/listing: pre-lookup conf values
- 4e967280 nntp: don't needlessly select group before ARTICLE in PR
#473 "Various"
- 67b88d24 Update polling dependency from "2.8" to "3"
- 14d74f36 Update smol dependency from "1" to "2"
- b950fcea melib: Use IndexMap in VCard
- 32acc347 view: show signature verification properly
- ac1349b8 command: alias pwd to cwd
- 7c056e4b Retry loading mailbox on recoverable error in PR #481
"Retry loading mailbox on recoverable error"
- cbafdcf7 terminal: color report WIP
- 4a26cfa1 logging: disable tracing from output
- 90974e7c imap: cache miss if row env hash != row hash
- 4c44c440 melib: #[ignore] shellexpand tests
- dc9e91df contacts/editor: Use FormButtonAction in form
- c0511901 Update debian/meli.{docs,examples} and Cargo exclude
- 592ce159 mbox: use Uuid::nil() as default envelope from
- 6eeb4571 nntp: make all fields public
- b27bac7f nntp: use DEFLATE when available by default
- 128b959f
nntp: prepend Newsgroups header if missing on NntpType::submit()
- a69122f8
pgp: use default sign/encrypt keys when no keys are selected
- e6fa7093 view/envelope: trim headers values to 3 lines maximum
- 7f0157a9 compose: make dialogs bigger in height in PR #490 "pgp: use
default sign/encrypt keys when no keys are selected"
- e032acfa view: pass filtered body to Composer as reply text in PR
#493 "view: pass filtered body to Composer as reply text"
- 49dcbc5e terminal: Extend Ask default actions, prompts
- cd2e4bf3 melib/utils: vendor urn crate
- 5915f125 backends: use IsSubscribedFn in method signatures
- 4f927bbe nntp: properly return all nntp mailboxes
- b930cb49 maildir: do not use rename_regex when only updating flags
- 27486f29 Accept newer versions of base64 dependency
- c3cac77d Update imap-codec dependency to 2.0.0-alpha.4
- 05f404ba jobs: do not use AtomicU64 in PR #505 "jobs: do not use
AtomicU64"
- 46916895 melib/gpgme: s/NULL/NUL when referring to NUL byte
- 81ace71b terminal/embedded: lift error checking earlier
- 24114811 manage: parse scroll_{left,right} actions
- d2559e42 imap: return all mailboxes, not just subscribed ones in PR
#509 "compose: fix add-attachment-file-picker"
- 320fddad melib/gpgme: disable layout tests on non-x86_64 hosts in PR
#511 "melib/gpgme: disable layout tests on non-x86_64 hosts"
- bcbcb012
melib/email/compose: ensure boundary always prefixed with CRLF
- d21c686d melib/attachments: Make AttachmentBuilder::set_raw generic
- d5d34579 melib/email/compose/tests: normalise test fn names
- e9ec6761 melib: make base64 dep mandatory
- 30405216 melib: make notmuch feature depend on maildir feature
- 35fa8e94 melib/imap: gracefully retry without DEFLATE on BYE in PR
#517 "Fix some unrelated bugs I found while debugging build failure
on armhf"
Refactoring
===========
- 20d73292 melib: replace async-stream dep with async-fn-stream
- 201081b6 meli/command: move tests to tests.rs
- 84cfa358 conf: remove need for global send_mail setting
- 7be8912c Cargo.tomls: make formatting more consistent
- e6877e89 melib/jmap: refactor some parser imports
- f7ec6d6b melib/jmap: implement mailbox rename
- 15d24ab0 meli/jobs: refactor spawn_{blocking,specialized} to spawn()
- 6ee148c0 Fix 1.80.0 clippy lints
- de72bc6a melib/error.rs: move network stuff to submodule
- a214a35c conf: refactor into submodules
- 978cefbb Replace Escape ascii char with hex literal
- 4b959f5c Remove pcre feature/dependency
- 036586a2 Update serde dependency to 1.0.205
- 191725b5
Fix some borrow checker error/warnings from upcoming 2024 edition
- 11798be8 Replace Envelope::message_id_display() with Display impls
- 394236ba email/address: Refactor References struct
- a7c73fc8 gpgme: refactor Rust interface, add tests
- 41e1fdd5 Fix cargo-derivefmt lints
- a44486d9 imap: fix minor clippy lint
- 0c0f8210 Add a "move to Trash" shortcut
- d20a9d0a Fix new clippy lints
- e9a72072 Remove unused/obsolete plugins code and mentions
- 2ddd28ee main.rs: always send a JobFinished event to all components
- 571ae390 pager.rs: don't set self dirty after filter selector in PR
#488 "view: fix nested filter jobs never being completed"
- 6bc0caf4 melib: remove redundant get_path_hash macro
- fc3308e4 melib: Add Mail::as_mbox() method
- b1f24cbe view/filters: forward events on child filters
- 1b201bf6 Remove GlobMatch trait, replace usage with Fnmatch
- 8af003ab Rename addressbook stuff to "contacts"
- 2069b4da errors: impl From<xdg::BaseDirectoriesError>
- 7dee32ae contacts: refactor Card to its own module
- 6d0d9680 jmap: move EmailObject state to Store
- 0c590bbc contact-editor: remove empty space in PR #495 "Add version
migration support"
- b2200ec3 Remove unused smtp tests in PR #501 "Apply patches from
upstream debian package"
- ae294945 remove unused module file
- 3558db51 Move jobs and mailbox management Components together
- 3a931035 command: move Composer actions under TabActions
- 441fda56 terminal: move TextPresentation trait to melib
- ee897942 lints: deny clippy::or_fun_call
- 0d088962 lints: Address clippy::too_long_first_doc_paragraph
- ecc9b482 Small repo cleanups
Documentation
=============
- a83b4176 meli.1: small fixes
- 72dea6f3 Manpage fixes
- a55f65e1
meli.conf.5: Fix wrong default value type in default_header_values
- 57b45a9c docs/historical-manpages: add DEP5 copyright file
- 00236b86 docs: add meli.conf.examples(5) WIP
- b88dc441 Comment out svgfeature; no need to ship it in PR #482
"milestone/0.8.8"
- b048c95a BUILD.md: add instructions for Android build
- 593ed22b pgp: perform gpgme's sign+encrypt manually in PR #494 "pgp:
perform gpgme's sign+encrypt manually"
- 50922d97 melib/README.md: update and fix feature table
- b912aabc docs: add examples of file picker usage in PR #516 "docs:
add examples of file picker usage"
Packaging
=========
- b55edd47 debian: update meli.docs and add meli.manpages
Miscellaneous Tasks
===================
- 1232e16a scripts/make_html_manual_page.py: don't prettify
- 6d520605 Vendor vobject crate
- b33433e4
Don't create backends as Box<dyn MailBackend>, but as Box<Self>
- 2001b4dd Make subscribed_mailboxes conf val optional
- 6cfe4da0 Enable rusqlite feature "modern_sqlite" always
- 707a129e Coalesce repeating TUI notification messages
- f036f95e scripts: add generate_release_changelog_entry.sh
Continuous Integration
======================
- 4684b601 CI: remove env vars from action names in PR #458 "Minor QoL
fixes"
- 7419b465 CI: unpin rust version after updating time dependency in PR
#460 "Update time dep to fix 1.80.0 breakage"
- 77da86eb CI: Update cargo-derivefmt version
- 1b3f2732 CI: Move build.yaml actions to Makefile.build
- 598a70f9 CI: move lints.yaml actions to Makefile.lint
- 7e800a8f
CI: move manifest_lints.yaml actions to Makefile.manifest-lints
- 98652110 CI: prepend printf commands with @
- ad79bf84 .gitea/Makefile.lint: attempt cargo-fmt with +nightly
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 19 Nov 2024 14:09:13 +0200
meli (0.8.7-1) bookworm; urgency=low
Contributors in alphabetical order:
- Andrei Zisu
- Damian Poddebniak
- Herby Gillot
- Manos Pitsidianakis
Added
=====
- 9fcb0a04 Add cargo-deny configuration file deny.toml
- 7e8d19af Add Envelope::sender_any
- 9ab404c5 Add pgp signed attachment support
- b4579075 Allow XOAUTH2 string passed as string
- 0ffe7fa5 Add text/plain or text/html arg for text decoding
- e107d613 Add prelude module for import cleanup
- 7200589a Add ErrorKind::NotFound
- 8c880dc7 Add {Error,ErrorKind}::is_recoverable()
- eb27773b Add pager.named_filters setting
- 84d93d65 Add support for ID extension (opt-in)
- af6838c2 Add metadata field to MailBackendCapabilities
- d1499242 Add From<Infallible> impl
- 814af0e9 Add --gzipped flag to man subcommand
- 475860c9 Accept - for stdio in `{create,test}_config`
- 86f9b213 Add timeout conf field in validate()
- dd525bd9 Use Error::is_recoverable
- 6e1fea80 Show suggestions on Unauthorized error
- 38620866 Detect DNS lookup std::io::Error
- a330ff96 Retry on DNS failure
- 2429f17b On invalid conf value, print what value is expected
- 6379fbe8 Add support for Undercurl attribute
- a13bf13f Add stub Undercurl support
- f5f1e068 Add UIDPLUS support
- afccebf3 Add AUTH=PLAIN support
- 9fb5bc41 Impl AUTH=ANONYMOUS (RFC4505)
Bug Fixes
=========
- ff3fe077 Fix new 1.79.0 clippy lints
- 430cbdfd Fix python errors
- e3c1656e Fix LOGINDISABLED support
- a82d1e1e Fix RowsState::rename_env stale data
- 8dc4465c Fix toml value ser after update of toml dependency
- 39e903b1 Fix issues with ShellExpandTrait
- 608301dc Expand save-to paths asap
- 100fa8b3 Fix edge case in ShellExpandTrait
- a85b3a08 Allow default_mailbox to be any mailbox
- 0dc24623 Fix one by off error on menu unread count
- 073aef86 Fix lints/errors when compiling specific feature combos
- 12695a00 Fix MSRV breakage
- 27ac3061 Fix tag support not being printed
- 97af00cd Respect timeout value from user configuration
- 824de287 Fix make_address! use
- f2e9cac3 Use suggested minimum for maxObjectsInGet
- 41d07fbc NewState in EmailImportResponse cannot be null
- 197132cc Support fetching with BODY[] for buggy servers
- 91fdef98 Return NotFound on cache miss
- 96cc02a0 Do not use ErrorKind::Configuration
- e96e9789 Don't discard pre-auth capabilities
- 122a2a4d Drain event_queue when mailbox made available
Changes
=======
- 27c4876f Prevent log flooding when drawing listing entries
- 7bdc8f52 Highlight_self also when self is sender
- c4f7b77a Rework attachment rendering logic with filters
- 1cce8c11 Accept invalid "+" CRLF cont req
- c04b593b Use BODY instead of RFC822
- 084a222a Remove subscribed mailboxes list
- 5b6c1aa8 Don't show all background jobs
- f9a3b333 Return NotFound on empty FETCH
- 15f3a3fb Retry fetch envelope only if err.is_recoverable()
- 15eeac51 Enable dns_cache, tcp_keepalive & tcp_nodelay
- 06437e60 Set not_yet_seen to 0 when inserting existing
- 0b113cdb Use MELI_FEATURES in all cargo invocations
Refactoring
===========
- 7856ea33 Transition more to imap-codec
- 6f61176a Remove unecessary mut modifier
- 3251e7bd Scrub skip_serializing_if from attributes
- ebc1fa3b Move module to self dir
- 5110813e Refactor MaildirOp and watch()
- a9122c6e Draw with x range argument
- 3ebf5510 Pass entire screen area when drawing overlay
- 2dc1721a Move signal handling stuff to submodule
- 738f7c46 Execute Opt subcommand in Opt::execute()
- 46df4b57 Remove unused function stub
- 52c75e92 Use HeaderName constants
- 6da4e2ec Replace stringify! in Debug impls with type checked macro
- 85a55ed6 Add some missing ErrorKinds to errors
- 8b568f6e Add if_in_state argument in Set::new()
- 1e2e3da0 Treat color input `; ;` as `; 0 ;`
- 7c47f702 Extract test and parser modules to files
- d40ee692 Extract tests mod from protocol_parser
- 1e50911c Add utils module to protocol_parser
- d3a45b34 Make default shared lib name a const
- a9e9d952 Change termination_string arg to Option
- fd76df78 Use MELI_CONFIG env var in mock tests
- 8552e499 Replace std::mem::{replace,take}
Documentation
=============
- dfc2bb43 Add link to MacPorts page for `meli`
- 97aa6a8e Replace obsolete .Tn macro with .Em
- a8e82a30 Add missing entries from JMAP
Miscellaneous Tasks
===================
- bbe2cffa Add rust-bindgen's friends.sh to scripts/
- a8956baf Update to `imap-codec` v2.0.0-alpha.1
- c99633e1 Update futures dependency 0.3.28 -> 0.3.30
- fe604bf0 Update "openssl" dependency to 0.10.64
- 9daf9437 Add test_cli_subcommands.rs
- 9f783d9a Pin assert_cmd ver to 2.0.13
- b7da1d0f Check all targets in cargo-msrv verify test
- 8a74920d Pin rust version to 1.79.0
-- Manos Pitsidianakis <manos@pitsidianak.is> Tue, 30 Jul 2024 14:21:31 +0300
meli (0.8.6-1) bookworm; urgency=low
Contributors in alphabetical order:
- euxane
- Manos Pitsidianakis
Added
=====
- 735b44f2 Add 'highlight_self' theme attribute
- e187bb3f Add tools subcommand with smtp shell for debugging
- 571bd984 Add proper imap-shell in tools subcommand for debugging
- 0e1e5b9e Add support for Alternate Scroll Mode (xterm)
- fe08d52a Add force_text_emoji_presentation option
Bug Fixes
=========
- 3de4908d man.7 Fix typo for toggle_expand_headers
- a8c7582f Fix ENVELOPE parsing in untagged responses
- c65635ef Fix compilation for macos
- 06ec2790 Fix str slice index panic
- f2b59a76 Add RequestUrlTemplate type
- 7eed944a Fix screwed up rfc8620 module split
- 74a3539f Fix degenerate OOB cell access
- e8e76970 Fix edge case with strings/linebreaking
- 81955187 Fix decryption error not shown
Refactoring
===========
- a9c3b151 Impl highlight_self in all index styles
- 57e3e643 Remove excessive right padding in flags
- a4ebe3b7 Add ErrorKind::Platform
- 4bdfb3a3 Disable Nagle's algorithm by default
- 4148aee5 Refactor smtp,draft errors and email tests
- ed5a6b04 Add a symbols range to is_emoji check
- fc1122a2 Rename to backend_mailbox.rs
- 50ecade7 Merge rfc8620/tests.rs to tests.rs
- a78f3f26 Move submodules to jmap/
- f7838b1d Split to methods.rs and objects.rs
- 74f0d12a Remove obsolete imapshell.rs and smtp_conn.rs
- dce3852f Add capabilities module
- 7ba7dc70 Imports cleanup in all modules
- 45bfcf87 Minor refactors
- 77867aee Unwrap object module
- 33999fc6 Re-add Submission to USING
- 6be25ac3 Don't use client field for get/posts
Documentation
=============
- 4722d7cc Also mention server_password_command for jmap
Miscellaneous Tasks
===================
- 2bfe6086 Hide self from "add contacts" options
- 9ca34a68 Update MSRV to 1.70.0
- 50ff16c4 Add LIGHT, DARK constant theme keys
- 1abce964 Add Envelope::recipient_any method
- 671d35e2 Update mailin-embedded dependency to 0.8.2
- 39fbb164 Change info_message_{next,prev} shortcuts to '<, >'
- 58d73271 Change new mail text content
- f0d1b9cf Add ayllu mirror link
- 3bab5324 Improve Debug impl for ContentType etc
- e9dd6bec Comment out content
- 8dd87c1a Add ContentType::is_text_plain()
- 01bc62e0 Add new_plaintext method
-- Manos Pitsidianakis <manos@pitsidianak.is> Sat, 08 Jun 2024 11:47:40 +0300
meli (0.8.5-1) bookworm; urgency=low
Contributors in alphabetical order:
- Andrei Zisu
- Ethra
- Geert Stappers
- Guillaume Ranquet
- Manos Pitsidianakis
Added
=====
- 0e3a0c4b Add safe UI widget area drawing API
- 0114e695 Add next_search_result and previous_search_result shortcuts
- 0b468d88 Improve Error messages
- 5af2e1ee Add subcommand to print config file location
- 62aee464 Add subcommand to print log file location
- e2cdebe8 Add option to highlight self in mailing list threads
- cd448924 Add clear-selection command
- 3a5306e9 View manpages in pager inside meli
- a37d5fc1 Implement a key to command mapping
- ce4ba06c Add a flag set/unset command
- 148f0433 Implement flag set/unset action in UI
- 417b24cd Print invalid command on error
- 4e941a9e Add default_mailbox setting
- 974502c6 Impl Hash for Card
- ba7a97e9 Add x axis scroll support
- ccf6f9a2 Remember previous set index_style preferences
Bug Fixes
=========
- bcec745c Fix command and status bar drawing
- 62b8465f Fix ThreadView for new TUI API
- 28fa66cc Fix ThreadedListing for new TUI API
- 2c6f180d Fix macos compilation
- 24971d19 Fix compilation with 1.70.0 cargo
- 34a2d52e Fix rustdoc::redundant_explicit_links
- f63774fa Fix new clippy lints (1.75)
- 33408146 Fix feature permutation mis-compilations found with cargo-hack
- e3351d27 Fix set unseen updating all mboxes
- 8185f2cf Add deny clippy lints and fix them
- 7861fb04 Fix typos found with typos tool
- 64e60cb0 Fix select modifier regression
- 60f26f9d Fix some old pre-intradoc rustdoc links
- 1fe36192 Make conf validation recognize AccountSettings extra keys
- c332c2f5 Fix new clippy lints (mostly clippy::blocks_in_conditions)
- 070930e6 Fix auto index build when missing
- 26928e3a Fix compilation for macos
- 3884c0da Small typographic fixups
- b820bd6d Remove unused imap_trace! and fix comp
- a88b8c5e Debian/changelog warning fix
- 4ce616ae Fix lints.yaml rustup install step
- 264782d2 Various unimportant minor style/doc fixups
- 475609fe Make {prev,next}_entry shortcut behavior consistent
- a69c674c Fix new 1.77 clippy lints
- 48cb9ee2 Fix compilation for macos
- 8a16cf6d Fix wrong column index crash
- bc1b6531 Fix constant redrawing
- 29cc1bce Remove obsolete file melib/src/text/tables.rs.gz
- ab041898 Fix new warnings for 1.78.0
- 46e40856 Fix UIConfirmationDialog highlight printing
- 3b93fa8e Don't draw messages above embedded terminal
- 684fae3e Copy old content to new buf when resizing
- 5d915baa Use Screen::resize instead of CellBuffer::resize
- 6a66afe9 Make add contact dialog scrollable on overflow
- aa5737a0 Prevent drawing pager on embedded mode
- 07072e2e Prevent panic if envelope is deleted
- 8ddd673d Update all mailboxes
- 3691cd29 Send EnvelopeUpdate event after self.collection.update_flags()
- 1fcb1d59 Remove rerun when build.rs changes
- 933bf157 Ack \ as an atom
- a1cbb198 Return Results instead of panicking
- b5ddc397 Remove unwrap() from get_events() loop
Changes
=======
- 61a0c3c2 Do not clear selection after action
- 9af284b8 Don't hide unread count for mailboxes that are partly truncated
- 35408b16 Run pager filter asynchronously
- e80ea9c9 Changed default manpage install path
- 742f038f Move sent_mailbox to settings
- 86bbf1ea Refresh NotmuchMailbox counts when setting flags
- f0866a39 Make config error more user-friendly
- 11f3077b Add more possible values for manpage names
- 1eca34b3 Set lowest priority to shortcut command UIEvents
- 484712b0 Check for unrecoverable errors in is_online
- 8ec6f220 Use ShellExpandTrait::expand in more user-provided paths
Refactoring
===========
- 0500e451 Add missing EnvelopeRemove event handler
- ab14f819 Make write_string_to_grid a CellBuffer method
- e0adcdfe Move rest of methods under CellBuffer
- 0a74c7d0 Overhaul refactor
- 3b4acc15 Add tests
- 7eedd860 Remove address_list! macro
- f3e85738 Move build.rs scripts to build directory
- 77325486 Remove on-push hooks for actions w/ run on-pr
- 08518e1c Remove obsolete position.rs module
- ddab3179 Move tests to tests module
- 79520068 Remove doctests, add tests module
- 4e7b6656 Sqlite caching refactor
- b5fd3f57 Make self.view an Option
- a3aaec38 Remove unused imports
- 11a0586d Remove num_cpus dependency
- 8f3dee9b Extract mod manpages to standalone file
- 89c7972e Add suggestions to BadValue variant
- 35a9f33a Extract common FlagString logic
- 1b0bdd0a Split queries and mailbox into submodules
- 506ae9f5 Add ErrorKind::LinkedLibrary variant
- ebe1b3da Wrap *mut struct fields in NonNull<_>
- ca7d7bb9 Use message freeze/thaw for flag changes
- 4026e254 Add some doc comments
- 808aa494 Rename text_processing to text for the whole brevity thing
- bebb473d Derive extra traits for enums
- ab1b946f Don't print details if it's an empty string.
- f685726e Add backtrace field to ParsingError
- 73d5b24e Merge integration tests in one crate
- 31401fa3 Add LazyCountSet::contains method
- 0270db01 From<&[u8]> -> From<B: AsRef<[u9]>>
- 873a67d0 Replace erroneous use of set_err_kind with set_kind
- 51e3f163 Use Url instead of String in deserializing
- 8014af25 Reduce debug prints
- f31b5c40 Don't print raw bytes as escaped unicode
- 41e965b8 Split mbox/job stuff in submodules
- ec01a441 Turn some sync connections to unsync
- 3e914465 Store children process metadata
- c53a32de Re-enables horizontal thread view
- 36b7c00b Put doc text type names and co. in backtics
- 634bd191 Convert log prints to traces
- 1048ce68 Add hostname() utility function
- 7645ff1b Rename write_string{to_grid,}
- c2ae19d1 Return Option from current_pos
- b61fc3ab Add HelpView struct for shortcuts widget
- 3495ffd6 Change UIEvent::Notification structure
- 23c15261 Abstract envelope view filters away
- 031d0f7d Add area.is_empty() checks in cell iterators
- e37997d6 Store Link URL value in Link type
- b6f769b2 Add field names to row_attr! bool values
- 0da97dd8 Check row_updates in is_dirty()
- 6506fffb Rewrite email flag modifications
- 23507932 Update cache on set_flags
- 470cae6b Update thread cache on email flag modifications
- 84f3641e Re-add on-screen message display
- 54d21f25 Re-add contact list and editor support
- 458258e1 Re-enable compact listing style
- 1c1be7d6 Add display_name(), display_slice(), display_name_slice() methods
- 5dd71ef1 Upgrade JobsView component to new TUI API
- b5cc2a09 Upgrade MailboxManager component to new TUI API
- ed8a5de2 Re-enable EditAttachments component
- 77a8d9e2 Make ModSequence publicly accessible
- 64898a05 Make UIDStore constructor pub
Documentation
=============
- e4818803 Various manpage touchups and URL updates
- 38bca8f8 Mention use_oauth2=true for gmail oauth2
- 660022ce Add mailaddr.7 manpage
- c5e9e676 Add historical-manpages dir
- 5afc0785 Update README.md, DEVELOPMENT.md and create BUILD.md
- d018f07a Retouch manual pages
- 3adba40e Add macos manpage mirror url
Packaging
=========
- cd2ba80f Update metadata
- 5f8d7c80 Update deb-dist target command with author metadata
- 59c99fdc Update debian package metadata
- 97eb6363 Add dpkg --print-architecture to deb filename
- 7412c238 Bump meli version to 0.8.5-rc.3
- 500fe7f7 Update CHANGELOG.md
- 5ff4e8ae Run builds.yaml when any manifest file changes
- 0a617410 Split test.yaml to test.yaml and lints.yaml
- 3ba1603a Add manifest file only lints workflow
- 1617212c Add scripts/check_debian_changelog.sh lint
- c41f35fd Use actions/checkout@v3
- 876616d4 Use actions/upload-artifact@v3
- 2419f4bd Add debian package build workflow
- 10c3b0ea Bump version to 0.8.5-rc.1
- d16afc7d Bump version to 0.8.5-rc.2
- da251455 Bump meli version to 0.8.5-rc.2
Miscellaneous Tasks
===================
- c4344529 Add .git-blame-ignore-revs file
- f70496f1 Add codemeta.json
- b3079715 Disable flakey test_smtp()
- 8a95febb Set debuginfo=0 in test/lint builds
- 81d1c053 Add mandoc_lint.sh
- 8de8addd Add cfg for musl builds
- 70fc2b45 Update nix dependency to 0.27
- fd64fe0b Update codeberg.org URL
- 30a3205e Add clippy::doc_markdown
- c7aee725 Add clippy::doc_markdown
- b8b24282 Update all instances of old domains with meli-email.org
- ae96038f Make unicode-segmentation a hard dependency
- 255e9376 Update linkify dep from 0.8.1 to 0.10.0
- dedee908 Update notify dep from 4.0.17 to 6.1.1
- c1c41c91 Update README.md and add Codeberg mirror
- 71f3ffe7 Update Makefile
- 63a63253 Use type alias for c_char
- c751b2e8 Re-enable conversations listing style
- 3a709794 Update minimum rust version from 1.65.0 to 1.68.2
- f900dbea Use cargo-derivefmt to sort derives alphabetically
- e19f3e57 Cargo-sort all Cargo.toml files
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 05 May 2024 18:46:42 +0300
meli (0.8.5-rc.3-1) bookworm; urgency=low
* Update to 0.8.5-rc.3
-- Manos Pitsidianakis <manos@pitsidianak.is> Sun, 10 Dec 2023 15:22:18 +0000
meli (0.8.5-rc.2-1) bookworm; urgency=low
* Update to 0.8.5-rc.2
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 4 Dec 2023 19:34:00 +0200
meli (0.8.4-1) bookworm; urgency=low
* Update to 0.8.4
-- Manos Pitsidianakis <manos@pitsidianak.is> Mon, 27 Nov 2023 19:34:00 +0200
meli (0.7.2-1) bullseye; urgency=low
Added
@ -28,7 +898,7 @@ meli (0.7.1-1) bullseye; urgency=low
- melib/nntp: implement refresh
- melib/nntp: update total/new counters on new articles
- melib/nntp: implement NNTP posting
- configs: throw error on extra unusued conf flags in some imap/nntp
- configs: throw error on extra unused conf flags in some imap/nntp
- configs: throw error on missing `composing` section with explanation
Fixed

1
debian/compat vendored
View file

@ -1 +0,0 @@
11

16
debian/control vendored
View file

@ -2,13 +2,19 @@ Source: meli
Section: mail
Priority: optional
Maintainer: Manos Pitsidianakis <manos@pitsidianak.is>
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
Build-Depends: debhelper-compat (=13), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
Standards-Version: 4.1.4
Homepage: https://meli.delivery
Rules-Requires-Root: no
Vcs-Git: https://git.meli-email.org/meli/meli.git
Vcs-Browser: https://git.meli-email.org/meli/meli
Homepage: https://meli-email.org
Package: meli
Architecture: any
Multi-Arch: foreign
Depends: ${misc:Depends}, ${shlibs:Depends}
Recommends: libnotmuch, xdg-utils (>=1.1.3-1)
Description: terminal mail client
Recommends: xdg-utils (>=1.1.3-1), w3m, mailcap
Suggests: libnotmuch5, notmuch, rss2email, xterm, neovim, msmtp
Provides: mail-reader, imap-client
Description: terminal mail client.
meli supports mbox, maildir, IMAP, JMAP, notmuch and NNTP (Usernet) with
TLS/SSL, SASL, GPG features.

4
debian/copyright vendored
View file

@ -1,11 +1,11 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: meli
Source: <https://git.meli.delivery/meli/meli>
Source: https://git.meli-email.org/meli/meli
#
# Please double check copyright with the licensecheck(1) command.
Files: *
Copyright: 2017-2020 Manos Pitsidianakis
Copyright: 2017-2023 Manos Pitsidianakis
License: GPL-3.0+
#----------------------------------------------------------------------------
# License file: COPYING

8
debian/extra/meli.desktop vendored Normal file
View file

@ -0,0 +1,8 @@
[Desktop Entry]
Name=meli
Exec=meli
Categories=Office;Network;Email;
Comment=Terminal mail client
NoDisplay=false
Terminal=true
Type=Application

7
debian/meli.bug-presubj vendored Normal file
View file

@ -0,0 +1,7 @@
WARNING: This package is not distributed by debian, it was generated from the source repository of meli.
Please do not report bugs to debian, but to the appropriate issue tracker for meli:
- https://git.meli-email.org/meli/meli/issues
- Send e-mail to the mailing list, "meli general" <meli-general@meli-email.org>
https://lists.meli-email.org/list/meli-general/

9
debian/meli.bug-script vendored Executable file
View file

@ -0,0 +1,9 @@
#!/bin/sh
echo "Including output of \`meli -v\` and \`meli compiled-with\`..."
LC_ALL=C meli -v >&3
echo "\nEnabled compile-time features"
echo "-----------------------------"
LC_ALL=C meli compiled-with >&3 || true

5
debian/meli.doc-base vendored Normal file
View file

@ -0,0 +1,5 @@
Document: meli
Title: meli E-mail Client Manual
Author: Various
Abstract: Manual for meli the terminal e-mail client.
Section: Network/Communication

8
debian/meli.docs vendored
View file

@ -1,4 +1,4 @@
meli/docs/meli.1
meli/docs/meli.7
meli/docs/meli.conf.5
meli/docs/meli-themes.5
meli/docs/*.1
meli/docs/*.5
meli/docs/*.7
meli/docs/external-tools.md

3
debian/meli.examples vendored Normal file
View file

@ -0,0 +1,3 @@
contrib/*
meli/docs/mail.vim
meli/docs/samples/*

3
debian/meli.manpages vendored Normal file
View file

@ -0,0 +1,3 @@
meli/docs/*.1
meli/docs/*.5
meli/docs/*.7

View file

@ -1,6 +1,6 @@
Description: Fix PREFIX env var in Makefile for use in Debian
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
Last-Update: 2023-03-06
Author: Manos Pitsidianakis <manos@pitsidianak.is>
Last-Update: 2024-11-19
Index: meli/Makefile
===================================================================
--- meli.orig/Makefile

View file

@ -1 +1,2 @@
fix-prefix-for-debian.patch
usr_bin_editor.patch

18
debian/patches/usr_bin_editor.patch vendored Normal file
View file

@ -0,0 +1,18 @@
Description: If EDITOR or VISUAL is not set, fall back to /usr/bin/editor, which is set by update-alternatives.
Author: Manos Pitsidianakis <manos@pitsidianak.is>
Last-Update: 2024-11-19
Index: meli/meli/src/subcommands.rs
===================================================================
--- meli.orig/meli/src/subcommands.rs
+++ meli/meli/src/subcommands.rs
@@ -56,9 +56,7 @@
pub fn edit_config() -> Result<()> {
let editor = std::env::var("EDITOR")
.or_else(|_| std::env::var("VISUAL"))
- .map_err(|err| {
- format!("Could not find any value in environment variables EDITOR and VISUAL. {err}")
- })?;
+ .unwrap_or_else(|_| "/usr/bin/editor".into());
let config_path = conf::get_config_file()?;
let mut cmd = Command::new(editor);

12
debian/rules vendored
View file

@ -1,14 +1,22 @@
#!/usr/bin/make -f
# You must remove unused comment lines for the released package.
#export DH_VERBOSE = 1
export RUSTUP_HOME=${HOME}/.rustup
export DH_VERBOSE = 1
export NO_MAN
#export DEB_BUILD_MAINT_OPTIONS = hardening=+all
#export DEB_CFLAGS_MAINT_APPEND = -Wall -pedantic
#export DEB_LDFLAGS_MAINT_APPEND = -Wl,--as-needed
export MELI_FEATURES = cli-docs sqlite3
#export MELI_FEATURES = cli-docs sqlite3
%:
dh $@ --with quilt
override_dh_auto_configure:
true
override_dh_auto_test:
true
#override_dh_auto_install:
# dh_auto_install -- prefix=/usr

191
deny.toml Normal file
View file

@ -0,0 +1,191 @@
# cargo-deny configuration
[graph]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
[output]
feature-depth = 1
[advisories]
ignore = [
{ id = "RUSTSEC-2021-0145", reason = "Affects Windows, which we do not support officially." },
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
#"MIT",
#"Apache-2.0",
#"Apache-2.0 WITH LLVM-exception",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], crate = "adler32" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The package spec the clarification applies to
#crate = "ring"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = [""]
# 1 or more gitlab.com organizations to allow git sources for
gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
bitbucket = [""]

View file

@ -8,17 +8,14 @@ edition = "2018"
[package.metadata]
cargo-fuzz = true
[[bin]]
name = "envelope_parse"
path = "fuzz_targets/envelope_parse.rs"
[dependencies]
libfuzzer-sys = "0.3"
[dependencies.melib]
path = "../melib"
features = ["unicode-algorithms"]
melib = { path = "../melib" }
# Prevent this from interfering with workspaces
[workspace]
members = ["."]
[[bin]]
name = "envelope_parse"
path = "fuzz_targets/envelope_parse.rs"

View file

@ -1,17 +1,18 @@
[package]
name = "meli"
version = "0.8.4"
version = "0.8.10"
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
edition = "2021"
rust-version = "1.65.0"
license = "GPL-3.0-or-later"
rust-version = "1.70.0"
license = "EUPL-1.2 OR GPL-3.0-or-later"
readme = "README.md"
description = "terminal mail client"
homepage = "https://meli.delivery"
repository = "https://git.meli.delivery/meli/meli.git"
description = "terminal e-mail client"
homepage = "https://meli-email.org"
repository = "https://git.meli-email.org/meli/meli.git"
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
categories = ["command-line-utilities", "email"]
default-run = "meli"
exclude = ["/docs/historical-manpages"]
[[bin]]
name = "meli"
@ -22,73 +23,69 @@ name = "meli"
path = "src/lib.rs"
[dependencies]
async-task = "^4.2.0"
aho-corasick = { version = "1.1.3" }
async-task = { version = "^4.2.0" }
bitflags = { version = "2.4", features = ["serde"] }
crossbeam = { version = "^0.8" }
flate2 = { version = "1", optional = true }
futures = "0.3.5"
indexmap = { version = "^1.6", features = ["serde-1"] }
futures = { version = "0.3.30", default-features = false, features = ["async-await", "executor", "std"] }
indexmap = { version = "^2.3", default-features = false, features = ["serde", "std"] }
itoa = { version = "1.0.11", default-features = false }
libc = { version = "0.2.125", default-features = false, features = ["extra_traits"] }
libz-sys = { version = "1.1", features = ["static"], optional = true }
linkify = { version = "^0.8", default-features = false }
melib = { path = "../melib", version = "0.8.4" }
nix = { version = "^0.24", default-features = false }
notify = { version = "4.0.1", default-features = false } # >:c
num_cpus = "1.12.0"
serde = "1.0.71"
serde_derive = "1.0.71"
serde_json = "1.0"
linkify = { version = "^0.10", default-features = false }
melib = { path = "../melib", version = "0.8.10", features = [] }
nix = { version = "0.29", default-features = false, features = ["signal", "poll", "term", "ioctl", "process"] }
regex = { version = "1" }
serde = { version = "1.0.71" }
serde_derive = { version = "1.0.71" }
serde_json = { version = "1.0" }
signal-hook = { version = "^0.3", default-features = false, features = ["iterator"] }
signal-hook-registry = { version = "1.2.0", default-features = false }
smallvec = { version = "^1.5.0", features = ["serde"] }
structopt = { version = "0.3.14", default-features = false }
svg_crate = { version = "^0.13", optional = true, package = "svg" }
structopt = { version = "0.3.26", default-features = false }
# svg_crate = { version = "^0.13", optional = true, package = "svg" }
termion = { version = "1.5.1", default-features = false }
toml = { version = "0.5.6", default-features = false, features = ["preserve_order"] }
xdg = "2.1.0"
[dependencies.pcre2]
# An [env] entry in .cargo/config.toml should force a static build instead of
# looking for a system library.
version = "0.2.3"
optional = true
[target.'cfg(target_os="linux")'.dependencies]
notify-rust = { version = "^4", default-features = false, features = ["dbus"], optional = true }
[build-dependencies]
flate2 = { version = "1", optional = true }
proc-macro2 = "1.0.37"
quote = "^1.0"
regex = "1"
syn = { version = "1", features = [] }
[dev-dependencies]
flate2 = { version = "1" }
regex = "1"
tempfile = "3.3"
toml = { version = "0.8", default-features = false, features = ["display","preserve_order","parse"] }
xdg = { version = "2.1.0" }
[features]
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "text-processing", "static"]
default = ["sqlite3", "notmuch", "smtp", "dbus-notifications", "gpgme", "cli-docs", "jmap", "static"]
notmuch = ["melib/notmuch"]
jmap = ["melib/jmap"]
sqlite3 = ["melib/sqlite3"]
smtp = ["melib/smtp"]
smtp-trace = ["smtp", "melib/smtp-trace"]
regexp = ["dep:pcre2"]
dbus-notifications = ["dep:notify-rust"]
cli-docs = ["dep:flate2"]
svgscreenshot = ["dep:svg_crate"]
text-processing = ["melib/unicode-algorithms"]
# svgscreenshot = ["dep:svg_crate"]
gpgme = ["melib/gpgme"]
# Static / vendoring features.
tls-static = ["melib/tls-static"]
http-static = ["melib/http-static"]
sqlite3-static = ["melib/sqlite3-static"]
dbus-static = ["dep:notify-rust", "notify-rust/d_vendored"]
libz-static = ["dep:libz-sys", "libz-sys/static"]
dbus-static = ["dep:notify-rust", "notify-rust?/d_vendored"]
libz-static = ["dep:libz-sys", "libz-sys?/static"]
static = ["tls-static", "http-static", "sqlite3-static", "dbus-static", "libz-static"]
# Print tracing logs as meli runs in stderr
# enable for debug tracing logs: build with --features=debug-tracing and export MELI_DEBUG_STDERR
debug-tracing = ["melib/debug-tracing"]
[build-dependencies]
flate2 = { version = "1", optional = true }
proc-macro2 = { version = "1.0.37" }
quote = { version = "^1.0" }
regex = { version = "1" }
syn = { version = "1", features = [] }
[dev-dependencies]
assert_cmd = { version = "=2.0.13" }
flate2 = { version = "1" }
predicates = { version = "3" }
regex = { version = "1" }
rusty-fork = { version = "0.3.0" }
tempfile = { version = "3.3" }
[target.'cfg(target_os="linux")'.dependencies]
notify-rust = { version = "^4", default-features = false, features = ["dbus"], optional = true }

View file

@ -22,12 +22,11 @@
extern crate proc_macro;
extern crate quote;
extern crate syn;
mod config_macros;
include!("config_macros.rs");
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
config_macros::override_derive(&[
override_derive(&[
("src/conf/pager.rs", "PagerSettings"),
("src/conf/listing.rs", "ListingSettings"),
("src/conf/notifications.rs", "NotificationsSettings"),
@ -40,7 +39,7 @@ fn main() {
{
use flate2::{Compression, GzBuilder};
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
use std::{env, fs::File, io::prelude::*, path::Path, process::Command};
use std::{env, io::prelude::*, path::Path};
let out_dir = env::var("OUT_DIR").unwrap();
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
@ -76,10 +75,20 @@ fn main() {
cl("docs/meli.1", "meli.txt.gz", false);
cl("docs/meli.conf.5", "meli.conf.txt.gz", false);
cl(
"docs/meli.conf.examples.5",
"meli.conf.examples.txt.gz",
false,
);
cl("docs/meli-themes.5", "meli-themes.txt.gz", false);
cl("docs/meli.7", "meli.7.txt.gz", false);
cl("docs/meli.1", "meli.mdoc.gz", true);
cl("docs/meli.conf.5", "meli.conf.mdoc.gz", true);
cl(
"docs/meli.conf.examples.5",
"meli.conf.examples.mdoc.gz",
true,
);
cl("docs/meli-themes.5", "meli-themes.mdoc.gz", true);
cl("docs/meli.7", "meli.7.mdoc.gz", true);
}

View file

@ -56,16 +56,21 @@ pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
#![allow(clippy::derivable_impls)]
//! This module is automatically generated by config_macros.rs.
//! This module is automatically generated by `config_macros.rs`.
use super::*;
use melib::HeaderName;
use indexmap::IndexSet;
use crate::conf::{*, data_types::*};
"##
.to_string();
let cfg_attr_default_attr_regex = Regex::new(r"\s*default\s*[,]").unwrap();
let cfg_attr_default_val_attr_regex = Regex::new(r#"\s*default\s*=\s*"[^"]*"\s*,\s*"#).unwrap();
let cfg_attr_skip_ser_attr_regex =
Regex::new(r#"\s*,?\s*skip_serializing_if\s*=\s*"[^"]*"\s*,?\s*"#).unwrap();
let cfg_attr_feature_regex = Regex::new(r"[(](?:not[(]\s*)?feature").unwrap();
'file_loop: for (filename, ident) in filenames {
@ -120,6 +125,14 @@ use melib::HeaderName;
f.tokens.clone().into_iter().next().unwrap()
{
let mut attr_inner_value = f.tokens.to_string();
if attr_inner_value.contains("skip_serializing_if") {
attr_inner_value = cfg_attr_skip_ser_attr_regex
.replace_all(&attr_inner_value, "")
.to_string();
let new_toks: proc_macro2::TokenStream =
attr_inner_value.parse().unwrap();
new_attr.tokens = quote! { #new_toks };
}
if cfg_attr_feature_regex.is_match(&attr_inner_value) {
attr_inner_value = cfg_attr_default_val_attr_regex
.replace_all(&attr_inner_value, "")
@ -154,6 +167,11 @@ use melib::HeaderName;
} else if attr_inner_value.starts_with("( default")
|| attr_inner_value.starts_with("(default")
{
if attr_inner_value.ends_with("default)")
|| attr_inner_value.ends_with("default )")
{
return None;
}
let rest = g.stream().into_iter().skip(2);
new_attr.tokens = quote! { ( #(#rest)*) };
match new_attr.tokens.to_string().as_str() {
@ -192,7 +210,7 @@ use melib::HeaderName;
#(#attrs_tokens)*
impl Default for #override_ident {
fn default() -> Self {
#override_ident {
Self {
#(#field_idents: None),*
}
}

View file

@ -2,7 +2,7 @@
## Sending mail with a command line tool
`composing.send_mail` can use either settings for an SMTP server or a shell
`send_mail` can use either settings for an SMTP server or a shell
command to which it pipes new mail to.
### `msmtp` and `send_mail`
@ -12,7 +12,6 @@ with many SMTP servers. It supports queuing and other small useful features.
See [the documentation](https://marlam.de/msmtp/msmtp.html).
```toml
[composing]
send_mail = 'msmtp --logfile=/home/user/.mail/msmtp.log --read-recipients
--read-envelope-from'
```
@ -120,3 +119,32 @@ The HTML of the e-mail is piped into `html_filter`'s standard input.
If your account's syncing is handled by an external tool, you can use the
refresh shortcuts within `meli` to call this tool with
`accounts.refresh_command`.
## Viewing binary attachments such as images inside your terminal
If you have a specific terminal tool that lets you pipe binary data to it and
it outputs command suitable for the terminal, you can use the `pipe-attachment`
command to view/preview attachments without leaving `meli` or opening a GUI app.
This requires the output to be interactive otherwise `meli` will run the tool
and immediately return, probably too quickly for you to notice the output in
your terminal. A general solution is to pipe the output to an interactive pager
like `less` which requires the user to exit it interactively.
The [`chafa`] tool can be used for images in this example:
Write a wrapper script that outputs the tool's output into a pager, for example
`less`. If the output contains ANSI escape codes (i.e. colors, or bold/italic
text) make sure to use `less -r` to preserve those codes.
```sh
#!/bin/sh
/bin/chafa "$@" | less -r
```
Save it somewhere as a file with executable permissions and you can use
`pipe-attachment 1 /path/to/your/chafa/wrapper` to view the first attachment as
an image with [`chafa`].
[`chafa`]: https://hpjansson.org/chafa/

View file

@ -0,0 +1,42 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Comment: This directory contains various manual pages that may be of use to meli users.
Files: mailaddr.7.gz
Comment: Sourced from debian manpages package.
Copyright: Copyright (c) 1983, 1987 The Regents of the University of California.
License: 6.5 (Berkeley) 2/14/89
Redistribution and use in source and binary forms are permitted
provided that the above copyright notice and this paragraph are
duplicated in all such forms and that any documentation,
advertising materials, and other materials related to such
distribution and use acknowledge that the software was developed
by the University of California, Berkeley. The name of the
University may not be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
Files: maildir.5.en.gz
Comment: Sourced from debian maildrop package.
Copyright: Copyright 1998 - 2007 Double Precision, Inc.
License: GPLv3 with OpenSSL linking extension
This software is released under the GPL, version 3 (see COPYING.GPL).
Additionally, compiling, linking, and/or using the OpenSSL toolkit in
conjunction with this software is allowed.
Files: mbox.5.en.gz
Comment: Sourced from debian mutt package.
Copyright: Copyright (C) 2000 Thomas Roessler <roessler@does-not-exist.org>
License: public-domain
This document is in the public domain and may be distributed and
changed arbitrarily.
Files: mbox.5qmail.en.gz
Comment: Sourced from (now obsolete) debian qmail package.
Copyright: D. J. Bernstein
License: From http://cr.yp.to/qmail/dist.html
I hereby place the qmail package (in particular, qmail-1.03.tar.gz,
with MD5 checksum 622f65f982e380dbe86e6574f3abcb7c) into the public
domain. You are free to modify the package, distribute modified
versions, etc.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,239 @@
.TH maildir 5
.SH "NAME"
maildir \- directory for incoming mail messages
.SH "INTRODUCTION"
.I maildir
is a structure for
directories of incoming mail messages.
It solves the reliability problems that plague
.I mbox
files and
.I mh
folders.
.SH "RELIABILITY ISSUES"
A machine may crash while it is delivering a message.
For both
.I mbox
files and
.I mh
folders this means that the message will be silently truncated.
Even worse: for
.I mbox
format, if the message is truncated in the middle of a line,
it will be silently joined to the next message.
The mail transport agent will try again later to deliver the message,
but it is unacceptable that a corrupted message should show up at all.
In
.IR maildir ,
every message is guaranteed complete upon delivery.
A machine may have two programs simultaneously delivering mail
to the same user.
The
.I mbox
and
.I mh
formats require the programs to update a single central file.
If the programs do not use some locking mechanism,
the central file will be corrupted.
There are several
.I mbox
and
.I mh
locking mechanisms,
none of which work portably and reliably.
In contrast, in
.IR maildir ,
no locks are ever necessary.
Different delivery processes never touch the same file.
A user may try to delete messages from his mailbox at the same
moment that the machine delivers a new message.
For
.I mbox
and
.I mh
formats, the user's mail-reading program must know
what locking mechanism the mail-delivery programs use.
In contrast, in
.IR maildir ,
any delivered message
can be safely updated or deleted by a mail-reading program.
Many sites use Sun's
.B Network F\fPa\fBil\fPur\fBe System
(NFS),
presumably because the operating system vendor does not offer
anything else.
NFS exacerbates all of the above problems.
Some NFS implementations don't provide
.B any
reliable locking mechanism.
With
.I mbox
and
.I mh
formats,
if two machines deliver mail to the same user,
or if a user reads mail anywhere except the delivery machine,
the user's mail is at risk.
.I maildir
works without trouble over NFS.
.SH "THE MAILDIR STRUCTURE"
A directory in
.I maildir
format has three subdirectories,
all on the same filesystem:
.BR tmp ,
.BR new ,
and
.BR cur .
Each file in
.B new
is a newly delivered mail message.
The modification time of the file is the delivery date of the message.
The message is delivered
.I without
an extra UUCP-style
.B From_
line,
.I without
any
.B >From
quoting,
and
.I without
an extra blank line at the end.
The message is normally in RFC 822 format,
starting with a
.B Return-Path
line and a
.B Delivered-To
line,
but it could contain arbitrary binary data.
It might not even end with a newline.
Files in
.B cur
are just like files in
.BR new .
The big difference is that files in
.B cur
are no longer new mail:
they have been seen by the user's mail-reading program.
.SH "HOW A MESSAGE IS DELIVERED"
The
.B tmp
directory is used to ensure reliable delivery,
as discussed here.
A program delivers a mail message in six steps.
First, it
.B chdir()\fPs
to the
.I maildir
directory.
Second, it
.B stat()s
the name
.BR tmp/\fItime.pid.host ,
where
.I time
is the number of seconds since the beginning of 1970 GMT,
.I pid
is the program's process ID,
and
.I host
is the host name.
Third, if
.B stat()
returned anything other than ENOENT,
the program sleeps for two seconds, updates
.IR time ,
and tries the
.B stat()
again, a limited number of times.
Fourth, the program
creates
.BR tmp/\fItime.pid.host .
Fifth, the program
.I NFS-writes
the message to the file.
Sixth, the program
.BR link() s
the file to
.BR new/\fItime.pid.host .
At that instant the message has been successfully delivered.
The delivery program is required to start a 24-hour timer before
creating
.BR tmp/\fItime.pid.host ,
and to abort the delivery
if the timer expires.
Upon error, timeout, or normal completion,
the delivery program may attempt to
.B unlink()
.BR tmp/\fItime.pid.host .
.I NFS-writing
means
(1) as usual, checking the number of bytes returned from each
.B write()
call;
(2) calling
.B fsync()
and checking its return value;
(3) calling
.B close()
and checking its return value.
(Standard NFS implementations handle
.B fsync()
incorrectly
but make up for it by abusing
.BR close() .)
.SH "HOW A MESSAGE IS READ"
A mail reader operates as follows.
It looks through the
.B new
directory for new messages.
Say there is a new message,
.BR new/\fIunique .
The reader may freely display the contents of
.BR new/\fIunique ,
delete
.BR new/\fIunique ,
or rename
.B new/\fIunique
as
.BR cur/\fIunique:info .
See
.B http://pobox.com/~djb/proto/maildir.html
for the meaning of
.IR info .
The reader is also expected to look through the
.B tmp
directory and to clean up any old files found there.
A file in
.B tmp
may be safely removed if it
has not been accessed in 36 hours.
It is a good idea for readers to skip all filenames in
.B new
and
.B cur
starting with a dot.
Other than this, readers should not attempt to parse filenames.
.SH "ENVIRONMENT VARIABLES"
Mail readers supporting
.I maildir
use the
.B MAILDIR
environment variable
as the name of the user's primary mail directory.
.SH "SEE ALSO"
mbox(5),
qmail-local(8)

View file

@ -17,7 +17,8 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI-THEMES 5
.Os
.Sh NAME
@ -31,15 +32,15 @@ comes with two themes,
.Ic dark
(default) and
.Ic light .
.sp
.Pp
Custom themes are defined as lists of key-values in the configuration files:
.Bl -bullet -compact
.Bl -item -compact -offset 2
.It
.Pa $XDG_CONFIG_HOME/meli/config.toml
.It
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
.El
.sp
.Pp
The application theme is defined in the configuration as follows:
.Bd -literal
[terminal]
@ -56,9 +57,9 @@ keys are settings for the
.Ic compact
mail listing style.
A setting contains three fields: fg for foreground color, bg for background color, and attrs for text attribute.
.sp
.Pp
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
.sp
.Pp
Each field contains a value, which may be either a color/attribute, a link (key name) or a valid alias.
An alias is a string starting with the \&"\&$\&" character and must be declared in advance in the
.Ic color_aliases
@ -69,10 +70,14 @@ An alias' value can be any valid value, including links and other aliases, as lo
In the case of a link the setting's real value depends on the value of the referred key.
This allows for defaults within a group of associated values.
Cyclic references in a theme results in an error:
.sp
.Pp
.Dl spooky theme contains a cycle: fg: mail.listing.compact.even -> mail.listing.compact.highlighted -> mail.listing.compact.odd -> mail.listing.compact.even
.Pp
Two themes are included by default, `light` and `dark`.
Two themes are included by default,
.Ql light
and
.Ql dark Ns
\&.
.Sh EXAMPLES
Specific settings from already defined themes can be overwritten:
.Bd -literal
@ -100,18 +105,18 @@ Custom themes can be included in your configuration files or be saved independen
.Pa $XDG_CONFIG_HOME/meli/themes/
directory as TOML files.
To start creating a theme right away, you can begin by editing the default theme keys and values:
.sp
.Pp
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
.sp
.Pp
.Pa new_theme.toml
will now include all keys and values of the "dark" theme.
.sp
.Pp
.Dl meli print-loaded-themes
.sp
.Pp
will print all loaded themes with the links resolved.
.Sh VALID ATTRIBUTE VALUES
Case-sensitive.
.Bl -bullet -compact
.Bl -dash -compact
.It
"Default"
.It
@ -123,6 +128,8 @@ Case-sensitive.
.It
"Underline"
.It
"Undercurl"
.It
"Blink"
.It
"Reverse"
@ -133,7 +140,7 @@ Any combo of the above separated by a bitwise XOR "\&|" eg "Dim | Italics"
.El
.Sh VALID COLOR VALUES
Color values are of type String with the following valid contents:
.Bl -bullet -compact
.Bl -dash -compact
.It
"Default" is the terminal default. (Case-sensitive)
.It
@ -146,8 +153,10 @@ Three character shorthand is also valid, e.g. #09c → #0099cc (Case-insensitive
name but with some modifications (for a full table see COLOR NAMES addendum) (Case-sensitive)
.El
.Sh NO COLOR
To completely disable ANSI colors, there are two options:
.Bl -bullet -compact
To completely disable
.Em ANSI
colors, there are two options:
.Bl -dash -compact
.It
Set the
.Ic use_color
@ -157,17 +166,22 @@ option (section
.It
The
.Ev NO_COLOR
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
environmental variable, when present (regardless of its value), prevents the addition of
.Em ANSI
color.
When the configuration value
.Ic use_color
is explicitly set to true by the user,
.Ev NO_COLOR
is ignored.
.El
.sp
In this mode, cursor locations (i.e., currently selected entries/items) will use the "reverse video" ANSI attribute to invert the terminal's default foreground/background colors.
.Pp
In this mode, cursor locations (i.e., currently selected entries/items) will use the
.Ql reverse video
.Em ANSI
attribute to invert the terminal's default foreground/background colors.
.Sh VALID KEYS
.Bl -bullet -compact
.Bl -dash -compact
.It
theme_default
.It
@ -312,7 +326,7 @@ pager.highlight_search_current
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Aqua:14:_:Black:0
Aquamarine1:122:_:Maroon:1
Aquamarine2:86:_:Green:2
@ -348,7 +362,7 @@ DarkMagenta1:91:_:SpringGreen6:29
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
DarkOliveGreen1:192:_:Turquoise4:30
DarkOliveGreen2:155:_:DeepSkyBlue3:31
DarkOliveGreen3:191:_:DeepSkyBlue4:32
@ -384,7 +398,7 @@ DeepPink4:125:_:Grey37:59
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
DeepPink6:162:_:MediumPurple6:60
DeepPink7:89:_:SlateBlue2:61
DeepPink8:53:_:SlateBlue3:62
@ -420,7 +434,7 @@ Grey19:236:_:DeepPink7:89
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Grey23:237:_:DarkMagenta:90
Grey27:238:_:DarkMagenta1:91
Grey3:232:_:DarkViolet1:92
@ -456,7 +470,7 @@ HotPink2:169:_:LightGreen:119
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
HotPink3:132:_:LightGreen1:120
HotPink4:168:_:PaleGreen1:121
IndianRed:131:_:Aquamarine1:122
@ -492,7 +506,7 @@ LightSlateGrey:103:_:DarkOliveGreen6:149
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
LightSteelBlue:147:_:DarkSeaGreen6:150
LightSteelBlue1:189:_:DarkSeaGreen3:151
LightSteelBlue3:146:_:LightCyan3:152
@ -528,7 +542,7 @@ NavajoWhite3:144:_:LightGoldenrod3:179
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Navy:4:_:Tan:180
NavyBlue:17:_:MistyRose3:181
Olive:3:_:Thistle3:182
@ -564,7 +578,7 @@ Purple5:55:_:Salmon1:209
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Red:9:_:LightCoral:210
Red1:196:_:PaleVioletRed1:211
Red2:124:_:Orchid2:212
@ -600,7 +614,7 @@ Tan:180:_:Grey30:239
allbox tab(:);
lb|lb|l|lb|lb
l l|l|l l.
name ↓:byte:_:name:byte ↓
name \(da:byte:_:name:byte \(da
Teal:6:_:Grey35:240
Thistle1:225:_:Grey39:241
Thistle3:182:_:Grey42:242
@ -621,16 +635,34 @@ Yellow6:148:_:Grey93:255
.Sh SEE ALSO
.Xr meli 1 ,
.Xr meli.conf 5
.Sh CONFORMING TO
TOML Standard v.0.5.0
.Lk https://toml.io/en/v0.5.0
.sp
.Lk https://no-color.org/
.Sh STANDARDS
.Bl -item -compact
.It
.Lk https://toml.io/en/v0.5.0 "TOML Standard v.0.5.0"
.It
.Lk https://no\-color.org/ "NO_COLOR: disabling ANSI color output by default"
.El
.Sh AUTHORS
Copyright 2017-2019
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.sp
.Lk https://meli.delivery
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View file

@ -17,6 +17,10 @@
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.de HorizontalRule
.\"\l'\n(.l\(ru1.25'
.sp
..
.de Shortcut
.Sm
.Aq \\$1
@ -40,12 +44,13 @@
.Ed
.sp
..
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI 1
.Os
.Sh NAME
.Nm meli
.Nd terminal e-mail client
.Nd terminal e\-mail client
.Em μέλι
is the Greek word for honey
.Sh SYNOPSIS
@ -64,37 +69,61 @@ Start meli with given configuration file.
Create configuration file in
.Pa path
if given, or at
.Pa $XDG_CONFIG_HOME/meli/config.toml
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
\&.
If
.Ar path
is
.Ar \-
the result is printed to the standard output stream.
.It Cm test-config Op Ar path
Test a configuration for syntax issues or missing options.
The configuration is read from
.Pa path
if given, or from
.Pa $XDG_CONFIG_HOME/meli/config.toml Ns
\&.
If
.Ar path
is
.Ar \-
the configuration is read from the standard input stream.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
.It Cm install-man Op Ar path
Install manual pages to the first location provided by
.Ev MANPATH
or
.Xr manpath 1 ,
unless you specify the directory as an argument.
.It Cm compiled-with
Print compile time feature flags of this binary.
.It Cm edit-config
Edit configuration files with
.Ev EDITOR
or
.Ev VISUAL Ns
\&.
.It Cm test-config Op Ar path
Test a configuration file for syntax issues or missing options.
.It Cm man Op Ar page
Print documentation page and exit (Piping to a pager is recommended).
.It Cm install-man Op Ar path
Install manual pages to the first location provided by
.Ar MANPATH
or
.Xr manpath 1 ,
unless you specify the directory as an argument.
.It Cm help
Prints help information or the help of the given subcommand(s).
.It Cm print-app-directories
Print all directories that
.Ns Nm
creates and uses.
.It Cm print-config-path
Print location of configuration file that will be loaded on normal app startup.
.It Cm print-default-theme
Print default theme keys and values in TOML syntax, to be used as a blueprint.
.It Cm print-loaded-themes
Print all loaded themes in TOML syntax.
.It Cm print-used-paths
Print all paths that are created and used.
.It Cm compiled-with
Print compile time feature flags of this binary.
.It Cm print-log-path
Print log file location.
.It Cm view
View mail from input file.
.El
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
is a terminal mail client aiming for extensive and user-friendly configurability.
.Bd -literal
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
@ -128,11 +157,28 @@ At any time, you may press
for a searchable list of all available actions and shortcuts, along with every possible setting and command that your version supports.
.Pp
The main visual navigation tool, the left-side sidebar may be toggled with
.ShortcutPeriod ` listing toggle_menu_visibility
.ShortcutPeriod \(ga listing toggle_menu_visibility
\&.
.Pp
Each mailbox may be viewed in 4 modes:
Plain views each mail individually, Threaded shows their thread relationship visually, Conversations collapses each thread of emails into a single entry, Compact shows one row per thread.
.Bl -dash -compact
.It
.Tg index-style-plain
.Em Plain
views each mail individually,
.It
.Tg index-style-threaded
.Em Threaded
shows their thread relationship visually,
.It
.Tg index-style-conversations
.Em Conversations
collapses each thread of e\-mails into a single entry,
.It
.Tg index-style-compact
.Em Compact
shows one row per thread.
.El
.Pp
If you're using a light color palette in your terminal, you should set
.Em theme = "light"
@ -148,6 +194,10 @@ See
for a more detailed tutorial on using
.Nm Ns
\&.
.Sh SHORTCUTS
See
.Xr meli.conf 5 SHORTCUTS
for shortcuts and their default values.
.Sh VIEWING MAIL
Open attachments by typing their index in the attachments list and then
.ShortcutPeriod a envelope_view open_attachment
@ -164,16 +214,32 @@ See
for the location of the mailcap files and
.Xr mailcap 5
for their syntax.
You can save individual attachments with the
.Command save-attachment Ar INDEX Ar path-to-file
command.
You can save individual attachments with the following command:
.Command save\-attachment Ar INDEX Ar path\-to\-file
.Ar INDEX
is the attachment's index in the listing.
If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
If the 0th index is provided, the entire message is saved.
If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message-id.
.Bl -tag -compact -width 8n
.It If the path provided is a directory, the attachment is saved with its filename set to the filename in the attachment, if any.
.It If the 0th index is provided, the entire message is saved.
.It If the path provided is a directory, the message is saved as an eml file with its filename set to the messages message\-id.
.El
.Pp
You can pipe individual attachments to binaries with the following command:
.Command pipe\-attachment Ar INDEX Ar binary Ar ARGS
Example usage with the
.Xr less 1
pager:
.D1 pipe\-attachment 0 less
If the binary does not wait for your input before exiting, you will probably
not see its output since you will return back to the user interface
immediately.
You can write a wrapper script that pipes your binary's output to
.Dl less
or
.Dl less \-r
if you want to preserve the ANSI escape codes in the pager's output.
.Sh SEARCH
Each e-mail storage backend has a default search method assigned.
Each e\-mail storage backend has a default search method assigned.
.Em IMAP
uses the SEARCH command,
.Em notmuch
@ -200,7 +266,7 @@ To enable sqlite3 indexing for an account set
to
.Em sqlite3
in the configuration file and to create the sqlite3 index issue command:
.Command index Ar ACCOUNT_NAME Ns
.Command reindex Ar ACCOUNT_NAME Ns
To search in the message body type your keywords without any special formatting.
To search in specific fields, prepend your search keyword with "field:" like so:
.Pp
@ -222,9 +288,8 @@ alias:
.Pc
String keywords with spaces must be quoted.
Quotes should always be escaped.
.sp
.Sy Important Notice about IMAP/JMAP
.sp
.Ss Important Notice about IMAP/JMAP
.HorizontalRule
To prevent downloading all your messages from your IMAP/JMAP server, don't set
.Em search_backend
to
@ -233,14 +298,17 @@ to
.Nm
will relay your queries to the IMAP server.
Expect a delay between query and response.
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable delay.
.Ss QUERY ABNF SYNTAX
.Bl -bullet
.HorizontalRule
.Bl -dash -compact
.It
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | query \&"or\&" query | query \&"and\&" query | not query
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | message_id | in_reply_to | references | header | all_addresses | subject | flags | has_attachment | query \&"or\&" query | query \&"and\&" query | not query
.It
.Li not = \&"not\&" | \&"!\&"
.It
.Li has_attachment = \&"has:attachment\&" | \&"has:attachments\&"
.It
.Li quoted = ALPHA / SP *(ALPHA / DIGIT / SP)
.It
.Li term = ALPHA *(ALPHA / DIGIT) | DQUOTE quoted DQUOTE
@ -251,6 +319,8 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
.It
.Li flagterm = flagval | flagval \&",\&" flagterm
.It
.Li flags = \&"flag:\&" flag | \&"flags:\&" flag | \&"tag:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
.It
.Li from = \&"from:\&" term
.It
.Li to = \&"to:\&" term
@ -259,16 +329,39 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable
.It
.Li bcc = \&"bcc:\&" term
.It
.Li alladdresses = \&"alladdresses:\&" term
.Li message_id = \&"message-id:\&" term | \&"msg-id:\&" term
.It
.Li in_reply_to = \&"in-reply-to:\&" term
.It
.Li references = \&"references:\&" term
.It
.Li header = \&"header:\&" field_name \&",\&" field_value
.It
.Li field_name = term
.It
.Li field_value = term
.It
.Li all_addresses = \&"all-addresses:\&" term
.It
.Li subject = \&"subject:\&" term
.It
.Li flags = \&"flags:\&" flag | \&"tags:\&" flag | \&"is:\&" flag
.El
.Sh FLAGS
.Nm
supports the basic maildir flags: passed, replied, seen, trashed, draft and flagged.
Flags can be searched with the
.Ns Ql flags:
prefix in a search query, and can be modified by
.Command flag set FLAG
and
.Command flag unset FLAG
.Sh TAGS
.Nm
supports tagging in notmuch and IMAP/JMAP backends.
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
Tags can be searched with the
.Ns Ql tags:
or
.Ns Ql flags:
prefix in a search query, and can be modified by
.Command tag add TAG
and
.Command tag remove TAG
@ -289,7 +382,8 @@ To reply to a mail, press
\&.
Both these actions open the mail composer view in a new tab.
.Ss Editing text
.Bl -bullet -compact
.HorizontalRule
.Bl -dash -compact
.It
Edit the header fields by selecting with the arrow keys and pressing
.Shortcut Enter general focus_in_text_field
@ -332,12 +426,14 @@ and to resume editing press the
command again.
.El
.Ss Attachments
.HorizontalRule
Attachments may be handled with the
.Cm add-attachment Ns
,
.Cm remove-attachment
commands (see below).
.Ss Sending
.HorizontalRule
Finally, pressing
.Shortcut s composing send_mail
will send your message according to your settings
@ -355,6 +451,7 @@ On complete failure to save your draft or sent message it will be saved in your
.Em tmp
directory instead and you will be notified of its location.
.Ss Drafts
.HorizontalRule
To save your draft without sending it, issue
.Em COMMAND
.Cm close
@ -366,11 +463,10 @@ To open a draft for further editing, select your draft in the mail listing and p
.Sh CONTACTS
.Nm
supports three kinds of contact backends:
.sp
.Bl -enum -compact -offset indent
.Bl -enum -compact
.It
an internal format that gets saved under
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
.Pa $XDG_DATA_HOME/meli/account_name/contacts Ns
\&.
.It
vCard files (v3, v4) through the
@ -389,7 +485,7 @@ compatible alias file in the option
.sp
See
.Xr meli.conf 5 ACCOUNTS
for the complete account configuration values.
for the complete account contact configuration values.
.Sh MODES
.Bl -tag -compact -width 8n
.It NORMAL
@ -409,8 +505,9 @@ captures all input as text input, and is exited with
.Cm Esc
key.
.El
.Ss COMMAND Mode
.Sh COMMAND
.Ss Mail listing commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm set Ar plain | threaded | compact | conversations
set the way mailboxes are displayed
@ -424,9 +521,9 @@ threaded:shows threads as a tree structure
plain:shows one row per mail, regardless of threading
.TE
.Bl -tag -width 36n
.It Cm sort Ar subject | date \ Ar asc | desc
.It Cm sort Oo Ar subject | date Oc Ar asc | desc
sort mail listing
.It Cm subsort Ar subject | date \ Ar asc | desc
.It Cm subsort Oo Ar subject | date Oc Ar asc | desc
sorts only the first level of replies.
.It Cm go Ar n
where
@ -445,37 +542,49 @@ Escape exits search results.
select threads matching
.Ar STRING
query.
.It Cm set seen, set unseen
.It Cm clear-selection
Clear current selection.
.It Cm set Ar seen | unseen
Set seen status of message.
.It Cm import Ar FILEPATH Ar MAILBOX_PATH
Import mail from file into given mailbox.
.It Cm copyto, moveto Ar MAILBOX_PATH
Copy or move to other mailbox.
.It Cm copyto, moveto Ar ACCOUNT Ar MAILBOX_PATH
Copy or move to another account's mailbox.
Copy or move to another account's mailbox.
.It Cm delete
Delete selected threads.
Delete selected entries.
.It Cm export-mbox Ar FILEPATH
Export selected threads to mboxcl2 file.
.It Cm create-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm create\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
create mailbox with given path.
Be careful with backends and separator sensitivity (eg IMAP)
.It Cm subscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm subscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
subscribe to mailbox with given path
.It Cm unsubscribe-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm unsubscribe\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
unsubscribe to mailbox with given path
.It Cm rename-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
.It Cm rename\-mailbox Ar ACCOUNT Ar MAILBOX_PATH_SRC Ar MAILBOX_PATH_DEST
rename mailbox
.It Cm delete-mailbox Ar ACCOUNT Ar MAILBOX_PATH
.It Cm delete\-mailbox Ar ACCOUNT Ar MAILBOX_PATH
deletes mailbox in the mail backend.
This action is unreversible.
This action is irreversible.
.El
.Ss Mail view commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm pipe Ar EXECUTABLE Ar ARGS
.It Cm pipe Ar EXECUTABLE Oo Ar ARGS Oc
pipe pager contents to binary
.It Cm filter Ar EXECUTABLE Ar ARGS
.It Cm filter Ar EXECUTABLE Oo Ar ARGS Oc
filter and display pager contents through command
.It Cm filter
select a filter from
.Ic pager.named_filters
configuration value
.Po
See
.Xr meli.conf 5 PAGER
for its syntax
.Pc
.It Cm list-post
post in list of viewed envelope
.It Cm list-unsubscribe
@ -484,11 +593,12 @@ unsubscribe automatically from list of viewed envelope
open list archive with
.Cm xdg-open
.El
.Ss composing mail commands
.Ss Composing mail commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm mailto Ar MAILTO_ADDRESS
Opens a composer tab with initial values parsed from the
.Li mailto:
.Li mailto :
address.
.It Cm add-attachment Ar PATH
in composer, add
@ -507,7 +617,11 @@ in
Launch command
.Ar CMD Ar ARGS Ns
\&.
The command should print file paths in stderr, separated by NULL bytes.
The command should print file paths in stdout, separated by NUL bytes.
Example usage with
.Xr fzf 1 Ns
:
.D1 add-attachment-file-picker < fzf --print0
.It Cm remove-attachment Ar INDEX
remove attachment with given index
.It Cm toggle sign
@ -519,7 +633,8 @@ for PGP configuration.
.It Cm save-draft
saves a copy of the draft in the Draft folder
.El
.Ss generic commands
.Ss Generic commands
.HorizontalRule
.Bl -tag -width 36n
.It Cm open-in-tab
opens envelope view in new tab
@ -543,10 +658,6 @@ Useful if you want to reload some settings without restarting
.Nm Ns
\&.
.El
.Sh SHORTCUTS
See
.Xr meli.conf 5 SHORTCUTS
for shortcuts and their default values.
.Sh EXIT STATUS
.Nm
exits with 0 on a successful run.
@ -558,13 +669,15 @@ catchall for general errors
process panic
.El
.Sh ENVIRONMENT
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
.It Ev EDITOR
Specifies the editor to use
.It Ev MELI_CONFIG
Override the configuration file
.It Ev NO_COLOR
When present (regardless of its value), prevents the addition of ANSI color.
When defined (regardless of its value), prevents the addition of
.Em ANSI
color.
The configuration value
.Ic use_color
overrides this.
@ -572,7 +685,7 @@ overrides this.
.Sh FILES
.Nm
uses the following parts of the XDG standard:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
.It Ev XDG_CONFIG_HOME
defaults to
.Pa ~/.config/
@ -582,17 +695,13 @@ defaults to
.El
.Pp
and appropriates the following locations:
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
.It Pa $XDG_CONFIG_HOME/meli/
User configuration directory
.It Pa $XDG_CONFIG_HOME/meli/config.toml
User configuration file, see
.Xr meli.conf 5
for its syntax and values.
.It Pa $XDG_CONFIG_HOME/meli/hooks/*
Reserved for event hooks.
.It Pa $XDG_CONFIG_HOME/meli/plugins/*
Reserved for plugin files.
.It Pa $XDG_CACHE_HOME/meli/*
Internal cached data used by meli.
.It Pa $XDG_DATA_HOME/meli/*
@ -621,75 +730,281 @@ Mailcap entries are searched for in the following files, in this order:
.It
.Pa /usr/local/etc/mailcap
.El
.Sh STANDARDS
.Bl -dash -compact
.It
.Rs
.%B XDG Base Directory Specification
.%O Version 0.8
.%A Waldo Bastian
.%A Allison Karlitskaya
.%A Lennart Poettering
.%A Johannes Löthberg
.%U https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
.%D May 08, 2021
.Re
.It
.Rs
.%B maildir
.%A Daniel J. Bernstein
.%U https://cr.yp.to/proto/maildir.html
.%D 1995
.Re
.It
.Rs
.%B RFC1524 A User Agent Configuration Mechanism For Multimedia Mail Format Information
.%O mailcap file
.%I Legacy
.%D September 01, 1993
.%A Dr. Nathaniel S. Borenstein
.%U https://datatracker.ietf.org/doc/rfc1524/
.Re
.It
.Rs
.%B RFC2047 MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
.%I IETF
.%D November 01, 1996
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc2047/
.Re
.It
.Rs
.%B RFC2183 Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
.%I Legacy
.%D August 01, 1997
.%A Rens Troost
.%A Steve Dorner
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc2183/
.Re
.It
.Rs
.%B RFC2369 The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
.%I Legacy
.%D July 01, 1998
.%A Joshua D. Baer
.%A Grant Neufeld
.%U https://datatracker.ietf.org/doc/rfc2369/
.Re
.It
.Rs
.%B RFC2426 vCard MIME Directory Profile
.%O vCard Version 3
.%I IETF
.%D September 01, 1998
.%A Frank Dawson
.%A Tim Howes
.%U https://datatracker.ietf.org/doc/rfc2426/
.Re
.It
.Rs
.%B RFC2971 IMAP4 ID extension
.%I IETF
.%D October 01, 2000
.%A Tim Showalter
.%U https://datatracker.ietf.org/doc/rfc2971/
.Re
.It
.Rs
.%B RFC3156 MIME Security with OpenPGP
.%I IETF
.%D August 01, 2001
.%A Thomas Roessler
.%A Michael Elkins
.%A Raph Levien
.%A Dave Del Torto
.%U https://datatracker.ietf.org/doc/rfc3156/
.Re
.It
.Rs
.%B RFC3461 Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
.%I IETF
.%D January 23, 2003
.%A Keith Moore
.%U https://datatracker.ietf.org/doc/rfc3461/
.Re
.It
.Rs
.%B RFC3501 INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
.%I IETF
.%D March 18, 2003
.%A Mark Crispin
.%U https://datatracker.ietf.org/doc/rfc3501/
.Re
.It
.Rs
.%B RFC3676 The Text/Plain Format and DelSp Parameters
.%I IETF
.%D February 19, 2004
.%A Randall Gellens
.%U https://datatracker.ietf.org/doc/rfc3676/
.Re
.It
.Rs
.%B RFC3691 Internet Message Access Protocol (IMAP) UNSELECT command
.%I IETF
.%D February 20, 2004
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc3691/
.Re
.It
.Rs
.%B RFC3977 Network News Transfer Protocol (NNTP)
.%I IETF
.%D October 26, 2006
.%A Clive Feather
.%U https://datatracker.ietf.org/doc/rfc3977/
.Re
.It
.Rs
.%B RFC4505 Anonymous Simple Authentication and Security Layer (SASL) Mechanism
.%I IETF
.%D June 12, 2006
.%A Kurt Zeilenga
.%U https://datatracker.ietf.org/doc/rfc4505/
.Re
.It
.Rs
.%B RFC4549 Synchronization Operations for Disconnected IMAP4 Clients
.%I IETF
.%D June 16, 2006
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc4549/
.Re
.It
.Rs
.%B RFC4616 The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
.%I IETF
.%D August 31, 2006
.%A Kurt Zeilenga
.%U https://datatracker.ietf.org/doc/rfc4616/
.Re
.It
.Rs
.%B RFC4954 SMTP Service Extension for Authentication
.%I IETF
.%D July 23, 2007
.%A Rob Siemborski
.%A Alexey Melnikov
.%U https://datatracker.ietf.org/doc/rfc4954/
.Re
.It
.Rs
.%B RFC5321 Simple Mail Transfer Protocol
.%I IETF
.%D October 01, 2008
.%A Dr. John C. Klensin
.%U https://datatracker.ietf.org/doc/rfc5321/
.Re
.It
.Rs
.%B RFC5322 Internet Message Format
.%I IETF
.%D October 01, 2008
.%A Pete Resnick
.%U https://datatracker.ietf.org/doc/rfc5322/
.Re
.It
.Rs
.%B RFC6048 Network News Transfer Protocol (NNTP) Additions to LIST Command
.%I IETF
.%D November 22, 2010
.%A Julien ÉLIE
.%U https://datatracker.ietf.org/doc/rfc6048/
.Re
.It
.Rs
.%B RFC6152 SMTP Service Extension for 8-bit MIME Transport
.%I IETF
.%D March 07, 2011
.%A Dave Crocker
.%A Dr. John C. Klensin
.%A Dr. Marshall T. Rose
.%A Ned Freed
.%U https://datatracker.ietf.org/doc/rfc6152/
.Re
.It
.Rs
.%B RFC6350 vCard Format Specification
.%O vCard Version 4
.%I IETF
.%D August 31, 2011
.%A Simon Perreault
.%U https://datatracker.ietf.org/doc/rfc6350/
.Re
.It
.Rs
.%B RFC6532 Internationalized Email Headers
.%I IETF
.%D February 17, 2012
.%A Abel Yang
.%A Shawn Steele
.%A Ned Freed
.%U https://datatracker.ietf.org/doc/rfc6532/
.Re
.It
.Rs
.%B RFC6868 Parameter Value Encoding in iCalendar and vCard
.%I IETF
.%D February 14, 2013
.%A Cyrus Daboo
.%U https://datatracker.ietf.org/doc/rfc6868/
.Re
.It
.Rs
.%B RFC7162 IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
.%I IETF
.%D May 23, 2014
.%A Alexey Melnikov
.%A Dave Cridland
.%U https://datatracker.ietf.org/doc/rfc7162/
.Re
.It
.Rs
.%B RFC8620 The JSON Meta Application Protocol (JMAP)
.%I IETF
.%D July 18, 2019
.%A Neil Jenkins
.%A Chris Newman
.%U https://datatracker.ietf.org/doc/rfc8620/
.Re
.It
.Rs
.%B RFC8621 The JSON Meta Application Protocol (JMAP) for Mail
.%I IETF
.%D August 08, 2019
.%A Neil Jenkins
.%A Chris Newman
.%U https://datatracker.ietf.org/doc/rfc8621/
.Re
.El
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr meli-themes 5 ,
.Xr meli 7 ,
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh CONFORMING TO
.Bl -bullet -compact
.It
XDG Standard
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
\&.
.It
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
.It
RFC 5322: Internet Message Format
.It
RFC 6532: Internationalized Email Headers
.It
RFC 2047: MIME (Multipurpose Internet Mail Extensions) Part Three: Message Header Extensions for Non-ASCII Text
.It
RFC 3676: The Text/Plain Format and DelSp Parameters
.It
RFC 3156: MIME Security with OpenPGP
.It
RFC 2183: Communicating Presentation Information in Internet Messages: The Content-Disposition Header Field
.It
RFC 2369: The Use of URLs as Meta-Syntax for Core Mail List Commands and their Transport through Message Header Fields
.It
.Li maildir
.Lk https://cr.yp.to/proto/maildir.html Ns
\&.
.It
RFC 5321: Simple Mail Transfer Protocol
.It
RFC 3461: Simple Mail Transfer Protocol (SMTP) Service Extension for Delivery Status Notifications (DSNs)
.It
RFC 4954: SMTP Service Extension for Authentication
.It
RFC 6152: SMTP Service Extension for 8-bit MIME Transport
.It
RFC 4616: The PLAIN Simple Authentication and Security Layer (SASL) Mechanism
.It
RFC 3501: INTERNET MESSAGE ACCESS PROTOCOL - VERSION 4rev1
.It
RFC 3691: Internet Message Access Protocol (IMAP) UNSELECT command
.It
RFC 4549: Synch Ops for Disconnected IMAP4 Clients
.It
RFC 7162: IMAP Extensions: Quick Flag Changes Resynchronization (CONDSTORE) and Quick Mailbox Resynchronization (QRESYNC)
.It
RFC 8620: The JSON Meta Application Protocol (JMAP)
.It
RFC 8621: The JSON Meta Application Protocol (JMAP) for Mail
.It
RFC 3977: Network News Transfer Protocol (NNTP)
.It
RFC 6048: Network News Transfer Protocol (NNTP) Additions to LIST Command
.It
vCard Version 3, RFC 2426: vCard MIME Directory Profile
.It
vCard Version 4, RFC 6350: vCard Format Specification
.It
RFC 6868 Parameter Value Encoding in iCalendar and vCard
.El
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind (See COPYING for full copyright and warranty notices).
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
.Lk https://meli.delivery
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

View file

@ -40,22 +40,23 @@
.Pc Ns
..
.de Command
.Bd -offset 1n -ragged
.Bd -ragged -offset 1n
.Cm \\$*
.Ed
..
.Dd November 11, 2022
.\".Dd November 11, 2022
.Dd March 10, 2024
.Dt MELI 7
.Os
.Sh NAME
.Nm meli
.Nd Tutorial for the meli terminal e-mail client
.Nd Tutorial for the meli terminal e\-mail client
.Sh SYNOPSIS
.Nm
.Op ...
.Sh DESCRIPTION
.Nm
is a terminal mail client aiming for extensive and user-frendly configurability.
is a terminal mail client aiming for extensive and user\-friendly configurability.
.Bd -literal -offset center
^^ .-=-=-=-. ^^
^^ (`-=-=-=-=-`) ^^
@ -158,9 +159,9 @@ key.
.It EMBED
This is the mode of the embed terminal emulator.
To exit an embedded application, issue
.Aq Ctrl-C
.Aq Ctrl\-C
to kill it or
.Aq Ctrl-Z
.Aq Ctrl\-Z
to stop the program and follow the instructions on
.Nm
to exit.
@ -229,7 +230,7 @@ This is the view you will spend more time with in
\&.
.Pp
Press
.Shortcut ` listing toggle_menu_visibility
.Shortcut \(ga listing toggle_menu_visibility
to toggle the sidebars visibility.
.Pp
Press
@ -237,16 +238,16 @@ Press
to switch focus on the sidebar menu.
Press
.Shortcut Right listing focus_left
to switch focus on the e-mail list.
to switch focus on the e\-mail list.
.Pp
On the e-mail list, press
On the e\-mail list, press
.Shortcut k listing scroll_up
to scroll up, and
.Shortcut j listing scroll_down
to scroll down.
Press
.Shortcut Enter listing open_entry
to open an e-mail entry and
to open an e\-mail entry and
.Shortcut i listing exit_entry
to exit it.
.Bd -ragged
@ -294,9 +295,9 @@ See
for details.
.Pp
You can increase the sidebar's width with
.Shortcut Ctrl-p listing increase_sidebar
.Shortcut Ctrl\-p listing increase_sidebar
and decrease with
.ShortcutPeriod Ctrl-o listing decrease_sidebar
.ShortcutPeriod Ctrl\-o listing decrease_sidebar
\&.
.Bd -ragged
.Sy The status bar.
@ -310,7 +311,7 @@ and decrease with
The status bar shows which mode you are, and the status message of the current view.
In the pictured example, it shows the status of a mailbox called
.Dq Inbox
with lots of e-mails.
with lots of e\-mails.
.Bd -ragged
.Sy The number modifier buffer.
.Ed
@ -330,7 +331,7 @@ entries.
Another use of the number buffer is opening URLs inside the pager.
See
.Sx PAGER
for an explanation of interacting with URLs in e-mails.
for an explanation of interacting with URLs in e\-mails.
.Pp
Pressing numbers in
.Sy NORMAL
@ -343,16 +344,16 @@ There are four different list styles:
.Bl -hyphen -compact
.It
.Qq plain
which shows one line per e-mail.
which shows one line per e\-mail.
.It
.Qq threaded
which shows a threaded view with drawn tree structure.
.It
.Qq compact
which shows one line per thread which can include multiple e-mails.
which shows one line per thread which can include multiple e\-mails.
.It
.Qq conversations
which shows more than one line per thread which can include multiple e-mails with more details about the thread.
which shows more than one line per thread which can include multiple e\-mails with more details about the thread.
.El
.Bd -ragged
.Sy Plain view\&.
@ -411,9 +412,9 @@ which shows more than one line per thread which can include multiple e-mails wit
.Sy Performing actions on entries and/or selections\&.
.Pp
Press
.Shortcut v listing select_entry
.Shortcut V listing select_entry
to toggle the selection of a single entry.
.Qq select_entry
.Shortcut v listing select_motion
can be prefixed by a number modifier and affixed by a scrolling motion (up or down) to select multiple entries.
.Tg number-modifier
Simple set operations can be performed on a selection with these shortcut modifiers:
@ -421,13 +422,13 @@ Simple set operations can be performed on a selection with these shortcut modifi
.Bl -hyphen -compact
.It
Union modifier:
.Shortcut Ctrl-u listing union_modifier
.Shortcut Ctrl\-u listing union_modifier
.It
Difference modifier:
.Shortcut Ctrl-d listing diff_modifier
.Shortcut Ctrl\-d listing diff_modifier
.It
Intersection modifier:
.Shortcut Ctrl-i listing intersection_modifier
.Shortcut Ctrl\-i listing intersection_modifier
.El
.Pp
To set an entry as
@ -445,7 +446,11 @@ which also has its complement
.sp
action.
.Pp
For e-mail backends that support tags
For e\-mail backends that support flags you can use the following commands on entries and selections to modify them:
.Command flag set FLAG
.Command flag unset FLAG
.Pp
For e\-mail backends that support tags
.Po
like
.Qq IMAP
@ -463,10 +468,13 @@ you can use the following commands on entries and selections to modify them:
and
.Ic ignore_tags
for how to set tag colors and tag visibility)
You can clear the selection with the
.Aq Esc
key.
.Sh PAGER
You can open an e-mail entry by pressing
You can open an e\-mail entry by pressing
.ShortcutPeriod Enter listing open_entry
\&. This brings up the e-mail view with the e-mail content inside a pager.
\&. This brings up the e\-mail view with the e\-mail content inside a pager.
.Bd -literal -offset center
┌────────────────────────────────────────────────────────────┐
│Date: Sat, 21 May 2022 16:16:11 +0300 ▀│
@ -494,14 +502,14 @@ You can open an e-mail entry by pressing
└────────────────────────────────────────────────────────────┘
.Ed
.Bd -ragged -offset 3n
.Em The\ pager\ displaying\ an\ e-mail\&.
.Em The\ pager\ displaying\ an\ e\-mail\&.
.Ed
.Pp
The pager is simple to use.
Scroll with the following:
.Bl -hang -width 27n
.It Go to next pager page
.Shortcut PageDown pager page_down
.Shortcut PageDown pager page_down
.It Go to previous pager page
.Shortcut PageUp pager page_up
.It Scroll down pager.
@ -516,7 +524,7 @@ which will act as a multiplier.
.Pp
The pager can enter a special
.Em url
mode which will prefix all detected hyperlinks and e-mail addresses with a number inside square brackets
mode which will prefix all detected hyperlinks and e\-mail addresses with a number inside square brackets
.ShortcutPeriod u pager toggle_url_mode
\&.
Writing down a chosen number as a number modifier
@ -547,13 +555,13 @@ for more details
.Pc Ns
\&.
.Sh MAIL VIEW
Other things you can do when viewing e-mail:
.Bl -bullet -compact
Other things you can do when viewing e\-mail:
.Bl -dash -compact
.It
Most importantly, you can exit the mail view with:
.Shortcut i listing exit_entry
.It
Add addresses from the e-mail headers to contacts:
Add addresses from the e\-mail headers to contacts:
.Shortcut c envelope_view add_addresses_to_contacts
.It
Open an attachment by entering its index as a number modifier and pressing:
@ -569,39 +577,39 @@ Reply to envelope:
.Shortcut R envelope_view reply
.It
Reply to author:
.Shortcut Ctrl-r envelope_view reply_to_author
.Shortcut Ctrl\-r envelope_view reply_to_author
.It
Reply to all/Reply to list/Follow up:
.Shortcut Ctrl-g envelope_view reply_to_all
.Shortcut Ctrl\-g envelope_view reply_to_all
.It
Forward email:
.Shortcut Ctrl-f envelope_view forward
Forward e\-mail:
.Shortcut Ctrl\-f envelope_view forward
.It
Expand extra headers: (References and others)
.Shortcut h envelope_view toggle_expand_headerk
.Shortcut h envelope_view toggle_expand_headers
.It
View envelope source in a pager: (toggles between raw and decoded source)
.Shortcut M-r envelope_view view_raw_source
.Shortcut M\-r envelope_view view_raw_source
.It
Return to envelope_view if viewing raw source or attachment:
.Shortcut r envelope_view return_to_normal_view
.El
.Sh COMPOSING
To compose an e-mail, you can either start with an empty draft by pressing
To compose an e\-mail, you can either start with an empty draft by pressing
.Shortcut m listing new_mail
which opens a composer view in a new tab.
To reply to a specific e-mail, when in envelope view you can select the specific action you want to take:
To reply to a specific e\-mail, when in envelope view you can select the specific action you want to take:
.sp
.Bl -bullet -compact
.Bl -dash -compact
.It
Reply to envelope.
.Shortcut R envelope_view reply
.It
Reply to author.
.Shortcut Ctrl-r envelope_view reply_to_author
.Shortcut Ctrl\-r envelope_view reply_to_author
.It
Reply to all.
.Shortcut Ctrl-g envelope_view reply_to_all
.Shortcut Ctrl\-g envelope_view reply_to_all
.El
.sp
To launch your editor, press
@ -688,25 +696,29 @@ the\ actual\ embedding\ is\ seamless\&.
.Ed
.Ss composing mail commands
.Bl -tag -width 36n
.It Cm add-attachment Ar PATH
.It Cm add\-attachment Ar PATH
in composer, add
.Ar PATH
as an attachment
.It Cm add-attachment < Ar CMD Ar ARGS
.It Cm add\-attachment < Ar CMD Ar ARGS
in composer, pipe
.Ar CMD Ar ARGS
output into an attachment
.It Cm add-attachment-file-picker
.It Cm add\-attachment\-file\-picker
Launch command defined in the configuration value
.Ic file_picker_command
in
.Xr meli.conf 5 TERMINAL
.It Cm add-attachment-file-picker < Ar CMD Ar ARGS
.It Cm add\-attachment\-file\-picker < Ar CMD Ar ARGS
Launch command
.Ar CMD Ar ARGS Ns
\&.
The command should print file paths in stderr, separated by NULL bytes.
.It Cm remove-attachment Ar INDEX
The command should print file paths in stdout, separated by NUL bytes.
Example usage with
.Xr fzf Ns
:
.D1 add-attachment-file-picker < fzf --print0
.It Cm remove\-attachment Ar INDEX
remove attachment with given index
.It Cm toggle sign
toggle between signing and not signing this message.
@ -714,7 +726,7 @@ If the gpg invocation fails then the mail won't be sent.
See
.Xr meli.conf 5 PGP
for PGP configuration.
.It Cm save-draft
.It Cm save\-draft
saves a copy of the draft in the Draft folder
.El
.\" [ref:TODO]: add contacts section
@ -731,12 +743,26 @@ for documentation on how to theme
.Xr xdg-open 1 ,
.Xr mailcap 5
.Sh AUTHORS
Copyright 2017-2022
.An Manos Pitsidianakis Aq manos@pitsidianak.is
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
(See COPYING for full copyright and warranty notices.)
.Pp
.Lk https://meli.delivery
.Lk https://github.com/meli/meli
.Lk https://crates.io/crates/meli
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,227 @@
.\" meli - meli.conf.examples.5
.\"
.\" Copyright 2024 Manos Pitsidianakis
.\"
.\" This file is part of meli.
.\"
.\" meli is free software: you can redistribute it and/or modify
.\" it under the terms of the GNU General Public License as published by
.\" the Free Software Foundation, either version 3 of the License, or
.\" (at your option) any later version.
.\"
.\" meli is distributed in the hope that it will be useful,
.\" but WITHOUT ANY WARRANTY; without even the implied warranty of
.\" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
.\" GNU General Public License for more details.
.\"
.\" You should have received a copy of the GNU General Public License
.\" along with meli. If not, see <http://www.gnu.org/licenses/>.
.\"
.\" SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
.de HorizontalRule
.\"\l'\n(.l\(ru1.25'
.sp
..
.de LiteralStringValue
.Sm
.Po Qo
.Em Li \\$1
.Qc Pc
.Sm
..
.de LiteralStringValueRenders
.LiteralStringValue \\$1
.shift 1
.Bo
.Sm
Rendered as:
.Li r##
.Qo
\\$1
.Qc
.Li ##
.Bc
.Sm
..
.\".Dd November 11, 2022
.Dd November 22, 2024
.Dt MELI.CONF.EXAMPLES 5
.Os
.Sh NAME
.Nm meli.conf examples
.Nd Example configurations for various mail backends supported by the
.Xr meli 1
terminal e-mail client
.\"
.\"
.\"
.\"
.\"
.\".Sh SYNOPSIS
.\".Pa $XDG_CONFIG_HOME/meli/config.toml
.\".\"
.\".\"
.\".\"
.\".\"
.\".\"
.\".Sh DESCRIPTION
.Sh MAILDIR ACCOUNT
An example configuration:
.\"
.\"
.\"
.Bd -literal
[accounts.account-name]
root_mailbox = "/path/to/root/folder"
format = "Maildir"
listing.index_style = "Compact"
identity="email@example.com"
display_name = "Name"
send_mail = 'msmtp --read-recipients --read-envelope-from'
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
# Set mailbox-specific settings
[accounts.account-name.mailboxes]
"INBOX" = { alias="Inbox" } #inline table
"drafts" = { alias="Drafts" } #inline table
[accounts.account-name.mailboxes."foobar-devel"] # or a regular table
ignore = true # don't show notifications for this mailbox
.Ed
.\"
.\"
.\"
.Sh MBOX ACCOUNT
An example configuration:
.\"
.\"
.\"
.Bd -literal
[accounts.account-name]
root_mailbox = "/var/mail/username"
format = "mbox"
listing.index_style = "Compact"
identity="username@hostname.local"
composing.send_mail = '/bin/false'
.Ed
.Sh IMAP ACCOUNT
.Bd -literal
[accounts."account-name"]
root_mailbox = "INBOX"
format = "imap"
server_hostname="mail.example.com"
server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
server_username="username@example.com"
#server_port="993" # imaps
server_port="143" # STARTTLS
use_starttls=true #optional
send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
display_name = "Name Name"
identity = "username@example.com"
## show only specific mailboxes:
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
.Ed
.Ss Gmail account example
.Bd -literal
[accounts."account-name"]
root_mailbox = '[Gmail]'
format = "imap"
send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
server_hostname='imap.gmail.com'
server_password="password"
server_username="username@gmail.com"
server_port="993"
listing.index_style = "Conversations"
identity = "username@gmail.com"
display_name = "Name Name"
# Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
composing.store_sent_mail = false
.Ed
.Sh JMAP ACCOUNT
The
.Ic server_url
option must hold the address of the server's session endpoint.
.Bd -literal
[accounts."account-name"]
root_mailbox = "INBOX"
format = "jmap"
send_mail = 'server_submission'
server_url="http://localhost:8080"
server_username="user@hostname.local"
server_password="changeme"
identity = "user@hostname.local"
.Ed
.Ss fastmail.com account example
.Lk https://fastmail.com/ Fastmail
uses the
.Em Bearer token
authentication mechanism, so the option
.Ic use_token
must be enabled:
.Bd -literal
[accounts."fastmail-jmap"]
root_mailbox = "INBOX"
format = "jmap"
server_url="https://api.fastmail.com/jmap/session"
server_username="user@fastmail.com"
server_password="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
use_token=true
identity = "My Name <user@fastmail.com>"
send_mail = "server_submission"
.Ed
.Sh NOTMUCH ACCOUNT
TODO
.Sh NNTP / USENET ACCOUNT
TODO
.Sh SEE ALSO
.Xr meli.conf 5 ,
.Xr meli 1 ,
.Xr meli-themes 5
.Sh AUTHORS
Copyright 2017\(en2024
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
.Pp
Released under the GPL, version 3 or greater.
This software carries no warranty of any kind.
.Po
See
.Pa COPYING
for full copyright and warranty notices.
.Pc
.Ss Links
.Bl -item -compact
.It
.Lk https://meli\-email.org "Website"
.It
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
.It
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
.It
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
.It
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
.El
.\" [pager]
.\" filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
.\" html_filter = "w3m -I utf-8 -T text/html"
.\" [notifications]
.\" script = "notify-send"
.\" [composing]
.\" # required for sending e-mail
.\" send_mail = 'msmtp --read-recipients --read-envelope-from'
.\" #send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
.\" editor_command = 'vim +/^$'
.\" [shortcuts]
.\" [shortcuts.composing]
.\" edit = 'e'
.\" [shortcuts.listing]
.\" new_mail = 'm'
.\" set_seen = 'n'
.\" [terminal]
.\" theme = "light"

View file

@ -11,9 +11,12 @@
#[accounts.account-name]
#root_mailbox = "/path/to/root/mailbox"
#format = "Maildir"
#send_mail = 'msmtp --read-recipients --read-envelope-from'
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#listing.index_style = "Conversations" # or [plain, threaded, compact]
#identity="email@example.com"
#display_name = "Name"
## Need to explicitly list mailboxes of interest:
#subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
#
## Set mailbox-specific settings
@ -26,6 +29,7 @@
#[accounts.mbox]
#root_mailbox = "/var/mail/username"
#format = "mbox"
#send_mail = 'false'
#listing.index_style = "Compact"
#identity="username@hostname.local"
#
@ -33,6 +37,7 @@
#[accounts."imap"]
#root_mailbox = "INBOX"
#format = "imap"
#send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#server_hostname="mail.example.com"
#server_password="pha2hiLohs2eeeish2phaii1We3ood4chakaiv0hien2ahie3m"
#server_username="username@example.com"
@ -42,15 +47,14 @@
#listing.index_style = "Conversations"
#identity = "username@example.com"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
### match specific mailboxes:
### show only specific mailboxes, overriding the server's subscribed status.
##subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
#
## Setting up an account for an already existing notmuch database
##[accounts.notmuch]
##root_mailbox = "/path/to/folder" # where .notmuch/ directory is located
##format = "notmuch"
##send_mail = 'msmtp --read-recipients --read-envelope-from'
##listing.index_style = "conversations"
##identity="username@example.com"
##display_name = "Name Name"
@ -64,6 +68,7 @@
#[accounts."gmail"]
#root_mailbox = '[Gmail]'
#format = "imap"
#send_mail = { hostname = "smtp.gmail.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#server_hostname='imap.gmail.com'
#server_password="password"
#server_username="username@gmail.com"
@ -71,22 +76,18 @@
#listing.index_style = "Conversations"
#identity = "username@gmail.com"
#display_name = "Name Name"
### match every mailbox:
#subscribed_mailboxes = ["*" ]
#composing.send_mail = 'msmtp --read-recipients --read-envelope-from'
### Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
## Gmail auto saves sent mail to Sent folder, so don't duplicate the effort:
#composing.store_sent_mail = false
#
##[accounts."jmap account"]
##root_mailbox = "INBOX"
##format = "jmap"
##send_mail = 'server_submission'
##server_url="http://localhost:8080"
##server_username="user@hostname.local"
##server_password="changeme"
##listing.index_style = "Conversations"
##identity = "user@hostname.local"
##subscribed_mailboxes = ["*", ]
##composing.send_mail = 'server_submission'
#
#[pager]
#filter = "COLUMNS=72 /usr/local/bin/pygmentize -l email"
@ -128,12 +129,8 @@
#page_down = "PageDown"
#
#[composing]
##required for sending e-mail
#send_mail = 'msmtp --read-recipients --read-envelope-from'
##send_mail = { hostname = "smtp.example.com", port = 587, auth = { type = "auto", username = "user", password = { type = "command_eval", value = "gpg2 --no-tty -q -d ~/.passwords/user.gpg" } }, security = { type = "STARTTLS" } }
#editor_command = 'vim +/^$' # optional, by default $EDITOR is used.
#
#
#[pgp]
#auto_sign = false # always sign sent messages
#auto_verify_signatures = true # always verify signatures when reading signed e-mails

View file

@ -0,0 +1,73 @@
[terminal.themes.ibm-modern]
"theme_default" = { fg = "$Black100", bg = "$White0", attrs = "Default" }
"status.bar" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
"status.notification" = { fg = "$Black100", bg = "$Magenta40", attrs = "theme_default" }
"tab.focused" = { fg = "$White0", bg = "$Purple40", attrs = "theme_default" }
"tab.unfocused" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"tab.bar" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"widgets.list.header" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"widgets.form.label" = { fg = "theme_default", bg = "theme_default", attrs = "Bold" }
"widgets.form.field" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"widgets.form.highlighted" = { fg = "theme_default", bg = "$Cyan30", attrs = "theme_default" }
"widgets.options.highlighted" = { fg = "$Cyan10", bg = "$Teal30", attrs = "theme_default" }
"mail.sidebar" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
"mail.sidebar_account_name" = { fg = "mail.sidebar", attrs = "Bold" }
"mail.sidebar_unread_count" = { fg = "$Magenta40", bg = "$CoolGray10" }
"mail.sidebar_index" = { fg = "theme_default", bg = "theme_default" }
"mail.sidebar_highlighted" = { fg = "theme_default", bg = "$CoolGray10" }
"mail.sidebar_highlighted_unread_count" = { from = "mail.sidebar_highlighted" }
"mail.sidebar_highlighted_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar_highlighted_account" = { fg = "mail.sidebar_highlighted", bg = "mail.sidebar_highlighted", attrs = "theme_default" }
"mail.sidebar_highlighted_account_name" = { fg = "mail.sidebar_highlighted_account", bg = "mail.sidebar_highlighted_account", attrs = "Bold" }
"mail.sidebar_highlighted_account_unread_count" = { fg = "mail.sidebar_unread_count", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.sidebar_highlighted_account_index" = { fg = "mail.sidebar_index", bg = "mail.sidebar_highlighted_account", attrs = "theme_default" }
"mail.listing.compact.even" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.odd" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.odd_unseen" = { fg = "$Black100", bg = "theme_default", attrs = "theme_default" }
"mail.listing.compact.even_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
"mail.listing.compact.odd_selected" = { fg = "$CoolGray10", attrs = "theme_default" }
"mail.listing.compact.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
"mail.listing.compact.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
"mail.listing.conversations" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.subject" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.from" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.date" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.unseen" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.listing.conversations.highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "Bold" }
"mail.listing.conversations.selected" = { fg = "$CoolGray10", bg = "$CoolGray30", attrs = "theme_default" }
"mail.listing.plain.even" = { fg = "mail.listing.compact.even", bg = "mail.listing.compact.even", attrs = "theme_default" }
"mail.listing.plain.odd" = { fg = "mail.listing.compact.odd", bg = "mail.listing.compact.odd", attrs = "theme_default" }
"mail.listing.plain.even_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
"mail.listing.plain.odd_unseen" = { fg = "$Black100", bg = "$CoolGray30", attrs = "theme_default" }
"mail.listing.plain.even_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.plain.odd_selected" = { fg = "theme_default", bg = "LightCoral", attrs = "theme_default" }
"mail.listing.plain.even_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
"mail.listing.plain.odd_highlighted" = { fg = "theme_default", bg = "$CoolGray10", attrs = "theme_default" }
"mail.view.headers" = { fg = "$Black100", bg = "$Purple40", attrs = "theme_default" }
"mail.view.headers_names" = { fg = "$Black100", bg = "$Magenta40", attrs = "mail.view.headers" }
"mail.view.headers_area" = { fg = "theme_default", bg = "$Purple40", attrs = "theme_default" }
"mail.view.body" = { fg = "theme_default", bg = "theme_default", attrs = "theme_default" }
"mail.view.thread.indentation.a" = { fg = "theme_default", bg = "CornflowerBlue", attrs = "theme_default" }
"mail.view.thread.indentation.b" = { fg = "theme_default", bg = "Red1", attrs = "theme_default" }
"mail.view.thread.indentation.c" = { fg = "theme_default", bg = "Pink3", attrs = "theme_default" }
"mail.view.thread.indentation.d" = { fg = "theme_default", bg = "Gold1", attrs = "theme_default" }
"mail.view.thread.indentation.e" = { fg = "theme_default", bg = "Orange3", attrs = "theme_default" }
"mail.view.thread.indentation.f" = { fg = "theme_default", bg = "CadetBlue", attrs = "theme_default" }
"mail.listing.attachment_flag" = { fg = "$CoolGray30", bg = "theme_default", attrs = "theme_default" }
"mail.listing.thread_snooze_flag" = { fg = "$Magenta40", bg = "theme_default", attrs = "theme_default" }
"mail.listing.tag_default" = { fg = "$White0", bg = "$Black100", attrs = "Bold" }
"pager.highlight_search" = { fg = "theme_default", bg = "$Teal30", attrs = "Bold" }
"pager.highlight_search_current" = { fg = "$CoolGray10", bg = "$Teal30", attrs = "Bold" }
[terminal.themes.ibm-modern.color_aliases]
"Blue60" = "#0f62fe"
"Black100" = "#000000"
"White0" = "#ffffff"
"Cyan30" = "#82cfff"
"Purple40" = "#be95ff"
"Magenta40" = "#ff7eb6"
"Teal30" = "#3ddbd9"
"Cyan10" = "#e5f6ff"
"CoolGray10" = "#f2f4f8"
"CoolGray30" = "#c1c7cd"

File diff suppressed because it is too large Load diff

View file

@ -35,10 +35,10 @@ impl Account {
mailbox_hash,
flags.clone(),
)?;
let handle = self
.main_loop_handler
.job_executor
.spawn_specialized("set_flags".into(), fut);
let handle =
self.main_loop_handler
.job_executor
.spawn("set-flags".into(), fut, self.is_async());
let job_id = handle.job_id;
self.insert_job(
job_id,
@ -53,35 +53,40 @@ impl Account {
Ok(job_id)
}
#[cfg(not(feature = "sqlite3"))]
pub(super) fn update_cached_env(&mut self, _: Envelope, _: Option<EnvelopeHash>) {}
// #[cfg(not(feature = "sqlite3"))]
// pub(super) fn update_cached_env(&mut self, _: Envelope, _:
// Option<EnvelopeHash>) {}
#[cfg(feature = "sqlite3")]
pub(super) fn update_cached_env(&mut self, env: Envelope, old_hash: Option<EnvelopeHash>) {
if self.settings.conf.search_backend == crate::conf::SearchBackend::Sqlite3 {
let msg_id = env.message_id_display().to_string();
match crate::sqlite3::remove(old_hash.unwrap_or(env.hash()))
.map(|_| crate::sqlite3::insert(env, self.backend.clone(), self.name.clone()))
{
Ok(job) => {
let handle = self
.main_loop_handler
.job_executor
.spawn_blocking("sqlite3::remove".into(), job);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
Err(err) => {
log::error!("Failed to update envelope {} in cache: {}", msg_id, err);
}
}
if self.settings.conf.search_backend == SearchBackend::Sqlite3 {
let msg_id = env.message_id().to_string();
let name = self.name.clone();
let backend = self.backend.clone();
let fut = async move {
crate::sqlite3::AccountCache::remove(
name.clone(),
old_hash.unwrap_or_else(|| env.hash()),
)
.await?;
crate::sqlite3::AccountCache::insert(env, backend, name).await?;
Ok(())
};
let handle = self.main_loop_handler.job_executor.spawn(
"sqlite3::remove".into(),
fut,
crate::sqlite3::AccountCache::is_async(),
);
self.insert_job(
handle.job_id,
JobRequest::Generic {
name: format!("Update envelope {} in sqlite3 cache", msg_id).into(),
handle,
log_level: LogLevel::TRACE,
on_finish: None,
},
);
}
}
}

247
meli/src/accounts/jobs.rs Normal file
View file

@ -0,0 +1,247 @@
//
// meli - accounts module.
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use std::{borrow::Cow, collections::HashMap, pin::Pin};
use futures::stream::Stream;
use melib::{backends::*, email::*, error::Result, LogLevel};
use smallvec::SmallVec;
use crate::{is_variant, jobs::JoinHandle, StatusEvent};
pub enum MailboxJobRequest {
Mailboxes {
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
CreateMailbox {
path: String,
handle: JoinHandle<Result<(MailboxHash, HashMap<MailboxHash, Mailbox>)>>,
},
DeleteMailbox {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<HashMap<MailboxHash, Mailbox>>>,
},
RenameMailbox {
mailbox_hash: MailboxHash,
new_path: String,
handle: JoinHandle<Result<Mailbox>>,
},
SetMailboxPermissions {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetMailboxSubscription {
mailbox_hash: MailboxHash,
new_value: bool,
handle: JoinHandle<Result<()>>,
},
}
impl std::fmt::Debug for MailboxJobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::CreateMailbox { .. } => write!(f, "JobRequest::CreateMailbox"),
Self::DeleteMailbox { mailbox_hash, .. } => {
write!(f, "JobRequest::DeleteMailbox({})", mailbox_hash)
}
Self::RenameMailbox {
mailbox_hash,
new_path,
..
} => {
write!(f, "JobRequest::RenameMailbox {mailbox_hash} to {new_path} ")
}
Self::SetMailboxPermissions { .. } => {
write!(f, "JobRequest::SetMailboxPermissions")
}
Self::SetMailboxSubscription { .. } => {
write!(f, "JobRequest::SetMailboxSubscription")
}
Self::Mailboxes { .. } => write!(f, "JobRequest::Mailboxes"),
}
}
}
impl std::fmt::Display for MailboxJobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Mailboxes { .. } => write!(f, "Get mailbox list"),
Self::CreateMailbox { path, .. } => write!(f, "Create mailbox {}", path),
Self::DeleteMailbox { .. } => write!(f, "Delete mailbox"),
Self::RenameMailbox { new_path, .. } => write!(f, "Rename mailbox to {new_path}"),
Self::SetMailboxPermissions { .. } => write!(f, "Set mailbox permissions"),
Self::SetMailboxSubscription { .. } => write!(f, "Set mailbox subscription"),
}
}
}
impl MailboxJobRequest {
pub fn cancel(&self) -> Option<StatusEvent> {
match self {
Self::Mailboxes { handle } => handle.cancel(),
Self::CreateMailbox { handle, .. } => handle.cancel(),
Self::DeleteMailbox { handle, .. } => handle.cancel(),
Self::RenameMailbox { handle, .. } => handle.cancel(),
Self::SetMailboxPermissions { handle, .. } => handle.cancel(),
Self::SetMailboxSubscription { handle, .. } => handle.cancel(),
}
}
}
pub enum JobRequest {
Fetch {
mailbox_hash: MailboxHash,
#[allow(clippy::type_complexity)]
handle: JoinHandle<(
Option<Result<Vec<Envelope>>>,
Pin<Box<dyn Stream<Item = Result<Vec<Envelope>>> + Send + 'static>>,
)>,
},
Generic {
name: Cow<'static, str>,
log_level: LogLevel,
handle: JoinHandle<Result<()>>,
on_finish: Option<crate::types::CallbackFn>,
},
IsOnline {
handle: JoinHandle<Result<()>>,
},
Refresh {
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SetFlags {
env_hashes: EnvelopeHashBatch,
mailbox_hash: MailboxHash,
flags: SmallVec<[FlagOp; 8]>,
handle: JoinHandle<Result<()>>,
},
SaveMessage {
bytes: Vec<u8>,
mailbox_hash: MailboxHash,
handle: JoinHandle<Result<()>>,
},
SendMessage,
SendMessageBackground {
handle: JoinHandle<Result<()>>,
},
DeleteMessages {
env_hashes: EnvelopeHashBatch,
handle: JoinHandle<Result<()>>,
},
Watch {
handle: JoinHandle<Result<()>>,
},
Mailbox(MailboxJobRequest),
}
impl std::fmt::Debug for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Generic { name, .. } => write!(f, "JobRequest::Generic({})", name),
Self::Mailbox(inner) => std::fmt::Debug::fmt(inner, f),
Self::Fetch { mailbox_hash, .. } => {
write!(f, "JobRequest::Fetch({})", mailbox_hash)
}
Self::IsOnline { .. } => write!(f, "JobRequest::IsOnline"),
Self::Refresh { .. } => write!(f, "JobRequest::Refresh"),
Self::SetFlags {
env_hashes,
mailbox_hash,
flags,
..
} => f
.debug_struct(stringify!(JobRequest::SetFlags))
.field("env_hashes", &env_hashes)
.field("mailbox_hash", &mailbox_hash)
.field("flags", &flags)
.finish(),
Self::SaveMessage { .. } => write!(f, "JobRequest::SaveMessage"),
Self::DeleteMessages { .. } => write!(f, "JobRequest::DeleteMessages"),
Self::Watch { .. } => write!(f, "JobRequest::Watch"),
Self::SendMessage => write!(f, "JobRequest::SendMessage"),
Self::SendMessageBackground { .. } => {
write!(f, "JobRequest::SendMessageBackground")
}
}
}
}
impl std::fmt::Display for JobRequest {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Generic { name, .. } => write!(f, "{}", name),
Self::Mailbox(inner) => std::fmt::Display::fmt(inner, f),
Self::Fetch { .. } => write!(f, "Mailbox fetch"),
Self::IsOnline { .. } => write!(f, "Online status check"),
Self::Refresh { .. } => write!(f, "Refresh mailbox"),
Self::SetFlags {
env_hashes, flags, ..
} => write!(
f,
"Set flags for {} message{}: {:?}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" },
flags
),
Self::SaveMessage { .. } => write!(f, "Save message"),
Self::DeleteMessages { env_hashes, .. } => write!(
f,
"Delete {} message{}",
env_hashes.len(),
if env_hashes.len() == 1 { "" } else { "s" }
),
Self::Watch { .. } => write!(f, "Background watch"),
Self::SendMessageBackground { .. } | Self::SendMessage => {
write!(f, "Sending message")
}
}
}
}
impl JobRequest {
is_variant! { is_watch, Watch { .. } }
is_variant! { is_online, IsOnline { .. } }
is_variant! { is_any_fetch, Fetch { .. } }
pub fn is_fetch(&self, mailbox_hash: MailboxHash) -> bool {
matches!(self, Self::Fetch {
mailbox_hash: h, ..
} if *h == mailbox_hash)
}
pub fn cancel(&self) -> Option<StatusEvent> {
match self {
Self::Generic { handle, .. } => handle.cancel(),
Self::Mailbox(inner) => inner.cancel(),
Self::Fetch { handle, .. } => handle.cancel(),
Self::IsOnline { handle, .. } => handle.cancel(),
Self::Refresh { handle, .. } => handle.cancel(),
Self::SetFlags { handle, .. } => handle.cancel(),
Self::SaveMessage { handle, .. } => handle.cancel(),
Self::DeleteMessages { handle, .. } => handle.cancel(),
Self::Watch { handle, .. } => handle.cancel(),
Self::SendMessage => None,
Self::SendMessageBackground { handle, .. } => handle.cancel(),
}
}
}

View file

@ -0,0 +1,261 @@
//
// meli - accounts module.
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use indexmap::IndexMap;
use melib::{
backends::{Mailbox, MailboxHash},
error::Error,
log,
};
use smallvec::SmallVec;
use crate::{conf::FileMailboxConf, is_variant};
#[derive(Clone, Debug, Default)]
pub enum MailboxStatus {
Available,
Failed(Error),
/// first argument is done work, and second is total work
Parsing(usize, usize),
#[default]
None,
}
impl MailboxStatus {
is_variant! { is_available, Available }
is_variant! { is_parsing, Parsing(_, _) }
}
#[derive(Clone, Debug)]
pub struct MailboxEntry {
pub status: MailboxStatus,
pub name: String,
pub path: String,
pub ref_mailbox: Mailbox,
pub conf: FileMailboxConf,
}
impl MailboxEntry {
pub fn new(
status: MailboxStatus,
name: String,
ref_mailbox: Mailbox,
conf: FileMailboxConf,
) -> Self {
let mut ret = Self {
status,
name,
path: ref_mailbox.path().into(),
ref_mailbox,
conf,
};
match ret.conf.mailbox_conf.extra.get("encoding") {
None => {}
Some(v) if ["utf-8", "utf8"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {}
Some(v) if ["utf-7", "utf7"].iter().any(|e| v.eq_ignore_ascii_case(e)) => {
ret.name = melib::backends::utf7::decode_utf7_imap(&ret.name);
ret.path = melib::backends::utf7::decode_utf7_imap(&ret.path);
}
Some(other) => {
log::warn!(
"mailbox `{}`: unrecognized mailbox name charset: {}",
&ret.name,
other
);
}
}
ret
}
pub fn status(&self) -> String {
match self.status {
MailboxStatus::Available => format!(
"{} [{} messages]",
self.name(),
self.ref_mailbox.count().ok().unwrap_or((0, 0)).1
),
MailboxStatus::Failed(ref e) => e.to_string(),
MailboxStatus::None => "Retrieving mailbox.".to_string(),
MailboxStatus::Parsing(done, total) => {
format!("Parsing messages. [{}/{}]", done, total)
}
}
}
pub fn name(&self) -> &str {
if let Some(name) = self.conf.mailbox_conf.alias.as_ref() {
name
} else {
self.ref_mailbox.name()
}
}
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct MailboxNode {
pub hash: MailboxHash,
pub depth: usize,
pub indentation: u32,
pub has_sibling: bool,
pub children: Vec<MailboxNode>,
}
pub fn build_mailboxes_order(
tree: &mut Vec<MailboxNode>,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mailboxes_order: &mut Vec<MailboxHash>,
) {
tree.clear();
mailboxes_order.clear();
for (h, f) in mailbox_entries.iter() {
if f.ref_mailbox.parent().is_none() {
fn rec(
h: MailboxHash,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
depth: usize,
) -> MailboxNode {
let mut node = MailboxNode {
hash: h,
children: Vec::new(),
depth,
indentation: 0,
has_sibling: false,
};
for &c in mailbox_entries[&h].ref_mailbox.children() {
if mailbox_entries.contains_key(&c) {
node.children.push(rec(c, mailbox_entries, depth + 1));
}
}
node
}
tree.push(rec(*h, mailbox_entries, 0));
}
}
macro_rules! mailbox_eq_key {
($mailbox:expr) => {{
if let Some(sort_order) = $mailbox.conf.mailbox_conf.sort_order {
(0, sort_order, $mailbox.ref_mailbox.path())
} else {
(1, 0, $mailbox.ref_mailbox.path())
}
}};
}
tree.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
let mut stack: SmallVec<[Option<&MailboxNode>; 16]> = SmallVec::new();
for n in tree.iter_mut() {
mailboxes_order.push(n.hash);
n.children.sort_unstable_by(|a, b| {
if mailbox_entries[&b.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&b.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Greater
} else if mailbox_entries[&a.hash]
.conf
.mailbox_conf
.sort_order
.is_none()
&& mailbox_entries[&a.hash]
.ref_mailbox
.path()
.eq_ignore_ascii_case("INBOX")
{
std::cmp::Ordering::Less
} else {
mailbox_eq_key!(mailbox_entries[&a.hash])
.cmp(&mailbox_eq_key!(mailbox_entries[&b.hash]))
}
});
stack.extend(n.children.iter().rev().map(Some));
while let Some(Some(next)) = stack.pop() {
mailboxes_order.push(next.hash);
stack.extend(next.children.iter().rev().map(Some));
}
}
drop(stack);
for node in tree.iter_mut() {
fn rec(
node: &mut MailboxNode,
mailbox_entries: &IndexMap<MailboxHash, MailboxEntry>,
mut indentation: u32,
has_sibling: bool,
) {
node.indentation = indentation;
node.has_sibling = has_sibling;
let mut iter = (0..node.children.len())
.filter(|i| {
mailbox_entries[&node.children[*i].hash]
.ref_mailbox
.is_subscribed()
})
.collect::<SmallVec<[_; 8]>>()
.into_iter()
.peekable();
indentation <<= 1;
if has_sibling {
indentation |= 1;
}
while let Some(i) = iter.next() {
let c = &mut node.children[i];
rec(c, mailbox_entries, indentation, iter.peek().is_some());
}
}
rec(node, mailbox_entries, 0, false);
}
}

View file

@ -0,0 +1,439 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use melib::conf::ToggleFlag;
use super::*;
use crate::command::actions::MailboxOperation;
impl Account {
pub fn mailbox_operation(&mut self, op: MailboxOperation) -> Result<JobId> {
if self.settings.account.read_only {
return Err(Error::new("Account is read-only."));
}
match op {
MailboxOperation::Create(path) => {
let job = self
.backend
.write()
.unwrap()
.create_mailbox(path.to_string())?;
let handle = self.main_loop_handler.job_executor.spawn(
"create_mailbox".into(),
job,
self.is_async(),
);
let job_id = handle.job_id;
self.insert_job(
handle.job_id,
JobRequest::Mailbox(MailboxJobRequest::CreateMailbox { path, handle }),
);
Ok(job_id)
}
MailboxOperation::Delete(path) => {
if self.mailbox_entries.len() == 1 {
return Err(Error::new("Cannot delete only mailbox."));
}
let mailbox_hash = self.mailbox_by_path(&path)?;
let job = self.backend.write().unwrap().delete_mailbox(mailbox_hash)?;
let handle = self.main_loop_handler.job_executor.spawn(
"delete-mailbox".into(),
job,
self.is_async(),
);
let job_id = handle.job_id;
self.insert_job(
handle.job_id,
JobRequest::Mailbox(MailboxJobRequest::DeleteMailbox {
mailbox_hash,
handle,
}),
);
Ok(job_id)
}
MailboxOperation::Subscribe(path) => {
let mailbox_hash = self.mailbox_by_path(&path)?;
let job = self
.backend
.write()
.unwrap()
.set_mailbox_subscription(mailbox_hash, true)?;
let handle = self.main_loop_handler.job_executor.spawn(
"subscribe-mailbox".into(),
job,
self.is_async(),
);
let job_id = handle.job_id;
self.insert_job(
handle.job_id,
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
mailbox_hash,
new_value: true,
handle,
}),
);
Ok(job_id)
}
MailboxOperation::Unsubscribe(path) => {
let mailbox_hash = self.mailbox_by_path(&path)?;
let job = self
.backend
.write()
.unwrap()
.set_mailbox_subscription(mailbox_hash, false)?;
let handle = self.main_loop_handler.job_executor.spawn(
"unsubscribe-mailbox".into(),
job,
self.is_async(),
);
let job_id = handle.job_id;
self.insert_job(
job_id,
JobRequest::Mailbox(MailboxJobRequest::SetMailboxSubscription {
mailbox_hash,
new_value: false,
handle,
}),
);
Ok(job_id)
}
MailboxOperation::Rename(path, new_path) => {
let mailbox_hash = self.mailbox_by_path(&path)?;
let job = self
.backend
.write()
.unwrap()
.rename_mailbox(mailbox_hash, new_path.clone())?;
let handle = self.main_loop_handler.job_executor.spawn(
format!("rename-mailbox {path} to {new_path}").into(),
job,
self.is_async(),
);
let job_id = handle.job_id;
self.insert_job(
job_id,
JobRequest::Mailbox(MailboxJobRequest::RenameMailbox {
handle,
mailbox_hash,
new_path,
}),
);
Ok(job_id)
}
MailboxOperation::SetPermissions(_) => Err(Error::new("Not implemented.")),
}
}
pub fn process_mailbox_event(&mut self, job_id: JobId, mut job: MailboxJobRequest) {
macro_rules! try_handle {
($handle:ident, $binding:pat => $then:block) => {{
try_handle! { $handle, Err(err) => {
self.main_loop_handler
.job_executor
.set_job_success(job_id, false);
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification {
title: None,
body: format!("{}: {} failed", &self.name, job).into(),
kind: Some(NotificationType::Error(err.kind)),
source: Some(err),
}));
return;
},
$binding => $then
}
}};
($handle:ident, Err($err:pat) => $then_err: block, $binding:pat => $then:block) => {{
match $handle.chan.try_recv() {
_err @ Ok(None) | _err @ Err(_) => {
/* canceled */
#[cfg(debug_assertions)]
log::trace!(
"handle.chan.try_recv() for job {} returned {:?}",
job_id,
_err
);
self.main_loop_handler
.job_executor
.set_job_success(job_id, false);
}
Ok(Some(Err($err))) => $then_err,
Ok(Some(Ok($binding))) => $then,
}
}};
}
match job {
MailboxJobRequest::Mailboxes { ref mut handle } => {
if let Ok(Some(mailboxes)) = handle.chan.try_recv() {
if let Err(err) = mailboxes.and_then(|mailboxes| self.init(mailboxes)) {
if !err.is_recoverable() {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::Notification {
title: Some(self.name.to_string().into()),
source: Some(err.clone()),
body: err.to_string().into(),
kind: Some(NotificationType::Error(err.kind)),
},
));
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::AccountStatusChange(
self.hash,
Some(err.to_string().into()),
),
));
self.is_online.set_err(err);
self.main_loop_handler
.job_executor
.set_job_success(job_id, false);
return;
}
let mailboxes_job = self.backend.read().unwrap().mailboxes();
if let Ok(mailboxes_job) = mailboxes_job {
let handle = self.main_loop_handler.job_executor.spawn(
"list-mailboxes".into(),
mailboxes_job,
self.is_async(),
);
self.insert_job(
handle.job_id,
JobRequest::Mailbox(MailboxJobRequest::Mailboxes { handle }),
);
};
} else {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::AccountStatusChange(
self.hash,
Some("Loaded mailboxes.".into()),
),
));
}
}
}
MailboxJobRequest::CreateMailbox { ref mut handle, .. } => {
try_handle! { handle, (mailbox_hash, mut mailboxes) => {
self.main_loop_handler.send(ThreadEvent::UIEvent(
UIEvent::MailboxCreate((self.hash, mailbox_hash)),
));
let mut new = FileMailboxConf::default();
new.mailbox_conf.subscribe = ToggleFlag::InternalVal(true);
new.mailbox_conf.usage = if mailboxes[&mailbox_hash].special_usage()
!= SpecialUsageMailbox::Normal
{
Some(mailboxes[&mailbox_hash].special_usage())
} else {
let tmp = SpecialUsageMailbox::detect_usage(
mailboxes[&mailbox_hash].name(),
);
if let Some(tmp) = tmp.filter(|&v| v != SpecialUsageMailbox::Normal)
{
mailboxes.entry(mailbox_hash).and_modify(|entry| {
let _ = entry.set_special_usage(tmp);
});
}
tmp
};
// if new mailbox has parent, we need to update its children field
if let Some(parent_hash) = mailboxes[&mailbox_hash].parent() {
self.mailbox_entries
.entry(parent_hash)
.and_modify(|parent| {
parent.ref_mailbox =
mailboxes.remove(&parent_hash).unwrap();
});
}
let status = MailboxStatus::default();
self.mailbox_entries.insert(
mailbox_hash,
MailboxEntry::new(
status,
mailboxes[&mailbox_hash].path().to_string(),
mailboxes.remove(&mailbox_hash).unwrap(),
new,
),
);
self.collection
.threads
.write()
.unwrap()
.insert(mailbox_hash, Threads::default());
self.collection
.mailboxes
.write()
.unwrap()
.insert(mailbox_hash, Default::default());
build_mailboxes_order(
&mut self.tree,
&self.mailbox_entries,
&mut self.mailboxes_order,
);
}}
}
MailboxJobRequest::DeleteMailbox {
mailbox_hash,
ref mut handle,
..
} => {
try_handle! { handle, mut mailboxes => {
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::MailboxDelete((
self.hash,
mailbox_hash,
))));
if let Some(pos) =
self.mailboxes_order.iter().position(|&h| h == mailbox_hash)
{
self.mailboxes_order.remove(pos);
}
if let Some(pos) = self.tree.iter().position(|n| n.hash == mailbox_hash) {
self.tree.remove(pos);
}
if self.settings.sent_mailbox == Some(mailbox_hash) {
self.settings.sent_mailbox = None;
}
self.collection
.threads
.write()
.unwrap()
.remove(&mailbox_hash);
let deleted_mailbox =
self.mailbox_entries.shift_remove(&mailbox_hash).unwrap();
// if deleted mailbox had parent, we need to update its children field
if let Some(parent_hash) = deleted_mailbox.ref_mailbox.parent() {
self.mailbox_entries
.entry(parent_hash)
.and_modify(|parent| {
parent.ref_mailbox = mailboxes.remove(&parent_hash).unwrap();
});
}
self.collection
.mailboxes
.write()
.unwrap()
.remove(&mailbox_hash);
build_mailboxes_order(
&mut self.tree,
&self.mailbox_entries,
&mut self.mailboxes_order,
);
// [ref:FIXME] remove from settings as well
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification {
title: Some(
format!("{}: mailbox deleted successfully", &self.name).into(),
),
source: None,
body: "".into(),
kind: Some(NotificationType::Info),
}));
}}
}
MailboxJobRequest::RenameMailbox {
ref mut handle,
mailbox_hash,
ref mut new_path,
} => {
use indexmap::map::MutableKeys;
try_handle! { handle, mailbox => {
let new_hash = mailbox.hash();
if let Some((_, key, entry)) = self.mailbox_entries.get_full_mut2(&mailbox_hash) {
*key = new_hash;
*entry = MailboxEntry::new(entry.status.clone(), std::mem::take(new_path), mailbox, entry.conf.clone());
}
if let Some(key) = self.mailboxes_order.iter_mut().find(|k| **k == mailbox_hash) {
*key = new_hash;
}
if let Some((_, key, _)) = self.event_queue.get_full_mut2(&mailbox_hash) {
*key = new_hash;
}
{
let mut threads = self.collection.threads.write().unwrap();
if let Some(entry) = threads.remove(&mailbox_hash) {
threads.insert(new_hash, entry);
}
}
{
let mut mailboxes = self.collection.mailboxes.write().unwrap();
if let Some(entry) = mailboxes.remove(&mailbox_hash) {
mailboxes.insert(new_hash, entry);
}
}
build_mailboxes_order(
&mut self.tree,
&self.mailbox_entries,
&mut self.mailboxes_order,
);
}}
}
MailboxJobRequest::SetMailboxPermissions { ref mut handle, .. } => {
try_handle! { handle, _ => {
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification {
title: Some(
format!("{}: mailbox permissions set successfully", &self.name)
.into(),
),
source: None,
body: "".into(),
kind: Some(NotificationType::Info),
}));
}}
}
MailboxJobRequest::SetMailboxSubscription {
ref mut handle,
ref mailbox_hash,
ref new_value,
} => {
try_handle! { handle, () => {
if self.mailbox_entries.contains_key(mailbox_hash) {
self.mailbox_entries.entry(*mailbox_hash).and_modify(|m| {
m.conf.mailbox_conf.subscribe = if *new_value {
ToggleFlag::True
} else {
ToggleFlag::False
};
let _ = m.ref_mailbox.set_is_subscribed(*new_value);
});
self.main_loop_handler
.send(ThreadEvent::UIEvent(UIEvent::Notification {
title: Some(
format!(
"{}: `{}` has been {}subscribed.",
&self.name,
self.mailbox_entries[mailbox_hash].name(),
if *new_value { "" } else { "un" }
)
.into(),
),
source: None,
body: "".into(),
kind: Some(NotificationType::Info),
}));
}
}}
}
}
}
}

565
meli/src/accounts/tests.rs Normal file
View file

@ -0,0 +1,565 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use std::path::PathBuf;
use melib::{
backends::{prelude::*, Mailbox, MailboxHash},
error::Result,
maildir::MaildirType,
smol, MailboxPermissions, SpecialUsageMailbox,
};
use tempfile::TempDir;
use crate::{
accounts::{AccountConf, FileMailboxConf, MailboxEntry, MailboxStatus},
command::actions::MailboxOperation,
utilities::tests::{eprint_step_fn, eprintln_ok_fn},
};
#[test]
fn test_mailbox_utf7() {
#[derive(Debug)]
struct TestMailbox(String);
impl melib::BackendMailbox for TestMailbox {
fn hash(&self) -> MailboxHash {
unimplemented!()
}
fn name(&self) -> &str {
&self.0
}
fn path(&self) -> &str {
&self.0
}
fn children(&self) -> &[MailboxHash] {
unimplemented!()
}
fn clone(&self) -> Mailbox {
unimplemented!()
}
fn special_usage(&self) -> SpecialUsageMailbox {
unimplemented!()
}
fn parent(&self) -> Option<MailboxHash> {
unimplemented!()
}
fn permissions(&self) -> MailboxPermissions {
unimplemented!()
}
fn is_subscribed(&self) -> bool {
unimplemented!()
}
fn set_is_subscribed(&mut self, _: bool) -> Result<()> {
unimplemented!()
}
fn set_special_usage(&mut self, _: SpecialUsageMailbox) -> Result<()> {
unimplemented!()
}
fn count(&self) -> Result<(usize, usize)> {
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
self
}
}
for (n, d) in [
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
("&BB4EQgQ,BEAEMAQyBDsENQQ9BD0ESwQ1-", "Отправленные"),
] {
let ref_mbox = TestMailbox(n.to_string());
let mut conf: melib::MailboxConf = Default::default();
conf.extra.insert("encoding".to_string(), "utf7".into());
let entry = MailboxEntry::new(
MailboxStatus::None,
n.to_string(),
Box::new(ref_mbox),
FileMailboxConf {
mailbox_conf: conf,
..Default::default()
},
);
assert_eq!(&entry.path, d);
}
}
fn new_maildir_backend(
temp_dir: &TempDir,
acc_name: &str,
event_consumer: BackendEventConsumer,
with_root_mailbox: bool,
) -> Result<(PathBuf, AccountConf, Box<MaildirType>)> {
let root_mailbox = temp_dir.path().join("inbox");
{
std::fs::create_dir(&root_mailbox).expect("Could not create root mailbox directory.");
if with_root_mailbox {
for d in &["cur", "new", "tmp"] {
std::fs::create_dir(root_mailbox.join(d))
.expect("Could not create root mailbox directory contents.");
}
}
}
let subscribed_mailboxes = if with_root_mailbox {
vec!["inbox".into()]
} else {
vec![]
};
let mailboxes = if with_root_mailbox {
vec![(
"inbox".into(),
melib::conf::MailboxConf {
extra: indexmap::indexmap! {
"path".into() => root_mailbox.display().to_string(),
},
..Default::default()
},
)]
.into_iter()
.collect()
} else {
indexmap::indexmap! {}
};
let extra = if with_root_mailbox {
indexmap::indexmap! {
"root_mailbox".into() => root_mailbox.display().to_string(),
}
} else {
indexmap::indexmap! {}
};
let account_conf = melib::AccountSettings {
name: acc_name.to_string(),
root_mailbox: root_mailbox.display().to_string(),
format: "maildir".to_string(),
identity: "user@localhost".to_string(),
extra_identities: vec![],
read_only: false,
display_name: None,
order: Default::default(),
subscribed_mailboxes,
mailboxes,
manual_refresh: true,
extra,
};
let maildir = MaildirType::new(&account_conf, Default::default(), event_consumer)?;
Ok((root_mailbox, account_conf.into(), maildir))
}
#[test]
fn test_accounts_mailbox_by_path_error_msg() {
const ACCOUNT_NAME: &str = "test";
let eprintln_ok = eprintln_ok_fn();
let mut eprint_step_closure = eprint_step_fn();
macro_rules! eprint_step {
($($arg:tt)+) => {{
eprint_step_closure(format_args!($($arg)+));
}};
}
let temp_dir = TempDir::new().unwrap();
{
eprint_step!(
"Create maildir backend with a root mailbox, \"inbox\" which will be a valid maildir \
folder because it will contain cur, new, tmp subdirectories..."
);
let mut ctx = crate::Context::new_mock(&temp_dir);
let backend_event_queue = Arc::new(std::sync::Mutex::new(
std::collections::VecDeque::with_capacity(16),
));
let backend_event_consumer = {
let backend_event_queue = Arc::clone(&backend_event_queue);
BackendEventConsumer::new(Arc::new(move |ah, be| {
backend_event_queue.lock().unwrap().push_back((ah, be));
}))
};
let (root_mailbox, settings, maildir) =
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, true).unwrap();
eprintln_ok();
let name = maildir.account_name.to_string();
let account_hash = maildir.account_hash;
let backend = maildir as Box<dyn MailBackend>;
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
let contacts = melib::contacts::Contacts::new(name.to_string());
let mut account = super::Account {
hash: account_hash,
name: name.into(),
is_online: super::IsOnline::True,
mailbox_entries: Default::default(),
mailboxes_order: Default::default(),
tree: Default::default(),
contacts,
collection: backend.collection(),
settings,
main_loop_handler: ctx.main_loop_handler.clone(),
active_jobs: HashMap::default(),
active_job_instants: std::collections::BTreeMap::default(),
event_queue: IndexMap::default(),
backend_capabilities: backend.capabilities(),
backend: Arc::new(std::sync::RwLock::new(backend)),
};
account.init(ref_mailboxes).unwrap();
while let Ok(thread_event) = ctx.receiver.try_recv() {
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
if !account.process_event(&job_id) {
assert!(
ctx.accounts[0].process_event(&job_id),
"unclaimed job id: {:?}",
job_id
);
}
}
}
eprint_step!("Assert that mailbox_by_path(\"inbox\") returns the root mailbox...");
account.mailbox_by_path("inbox").unwrap();
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"box\") returns an error mentioning the root mailbox..."
);
assert_eq!(
account.mailbox_by_path("box").unwrap_err().to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"Some matching paths that were found: [\"inbox\"]. You can inspect the list \
of mailbox paths of an account with the manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
macro_rules! wait_for_job {
($job_id:expr) => {{
let wait_for = $job_id;
while let Ok(thread_event) = ctx.receiver.recv() {
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
if !account.process_event(&job_id) {
assert!(
ctx.accounts[0].process_event(&job_id),
"unclaimed job id: {:?}",
job_id
);
} else if job_id == wait_for {
break;
}
}
}
}};
}
eprint_step!(
"Create new mailboxes: \"Sent\", \"Trash\", \"Drafts\", \"Archive\", \"Outbox\", \
\"Archive/Archive (old)\"..."
);
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create("Sent".to_string()))
.unwrap());
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create("Trash".to_string()))
.unwrap());
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create("Drafts".to_string()))
.unwrap());
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create("Archive".to_string()))
.unwrap());
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create("Outbox".to_string()))
.unwrap());
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create(
"inbox/Archive/Archive (old)".to_string(),
))
.unwrap());
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"rchive\") returns an error and mentions matching \
archives with mailboxes with the least depth in the tree hierarchy of mailboxes \
mentioned first..."
);
assert_eq!(
account.mailbox_by_path("rchive").unwrap_err().to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"Some matching paths that were found: [\"inbox/Archive\", \
\"inbox/Archive/Archive (old)\"]. You can inspect the list of mailbox paths \
of an account with the manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
eprint_step!("Create \"inbox/Archive/Archive{{1,2,3,4,5,6,7,8,9,10}}\" mailboxes...");
for i in 1..=10 {
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create(format!(
"inbox/Archive/Archive{i}"
)))
.unwrap());
}
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"inbox/Archive/Archive{{n}}\") works, i.e. we have to \
specify the root prefix \"inbox\"..."
);
for i in 1..=10 {
account
.mailbox_by_path(&format!("inbox/Archive/Archive{i}"))
.unwrap();
account
.mailbox_by_path(&format!("Archive/Archive{i}"))
.unwrap_err();
}
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"rchive\") returns and error and truncates the matching \
mailbox paths to 5 maximum..."
);
assert_eq!(
account.mailbox_by_path("rchive").unwrap_err().to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"Some matching paths that were found: [\"inbox/Archive\", \
\"inbox/Archive/Archive1\", \"inbox/Archive/Archive2\", \
\"inbox/Archive/Archive3\", \"inbox/Archive/Archive4\"] and 7 others. You \
can inspect the list of mailbox paths of an account with the \
manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"inbox/Archive\") returns a valid result (since the \
root mailbox is a valid maildir folder)..."
);
account.mailbox_by_path("inbox/Archive").unwrap();
eprintln_ok();
eprint_step!("Cleanup maildir account with valid root mailbox...");
std::fs::remove_dir_all(root_mailbox).unwrap();
eprintln_ok();
}
{
eprint_step!(
"Create maildir backend with a root mailbox, \"inbox\" which will NOT be a valid \
maildir folder because it will NOT contain cur, new, tmp subdirectories..."
);
let mut ctx = crate::Context::new_mock(&temp_dir);
let backend_event_queue = Arc::new(std::sync::Mutex::new(
std::collections::VecDeque::with_capacity(16),
));
let backend_event_consumer = {
let backend_event_queue = Arc::clone(&backend_event_queue);
BackendEventConsumer::new(Arc::new(move |ah, be| {
backend_event_queue.lock().unwrap().push_back((ah, be));
}))
};
let (_root_mailbox, settings, maildir) =
new_maildir_backend(&temp_dir, ACCOUNT_NAME, backend_event_consumer, false).unwrap();
eprintln_ok();
let name = maildir.account_name.to_string();
let account_hash = maildir.account_hash;
let backend = maildir as Box<dyn MailBackend>;
let ref_mailboxes = smol::block_on(backend.mailboxes().unwrap()).unwrap();
eprint_step!("Assert that created account has no mailboxes at all...");
assert!(
ref_mailboxes.is_empty(),
"ref_mailboxes were not empty: {:?}",
ref_mailboxes
);
eprintln_ok();
let contacts = melib::contacts::Contacts::new(name.to_string());
let mut account = super::Account {
hash: account_hash,
name: name.into(),
is_online: super::IsOnline::True,
mailbox_entries: Default::default(),
mailboxes_order: Default::default(),
tree: Default::default(),
contacts,
collection: backend.collection(),
settings,
main_loop_handler: ctx.main_loop_handler.clone(),
active_jobs: HashMap::default(),
active_job_instants: std::collections::BTreeMap::default(),
event_queue: IndexMap::default(),
backend_capabilities: backend.capabilities(),
backend: Arc::new(std::sync::RwLock::new(backend)),
};
account.init(ref_mailboxes).unwrap();
while let Ok(thread_event) = ctx.receiver.try_recv() {
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
if !account.process_event(&job_id) {
assert!(
ctx.accounts[0].process_event(&job_id),
"unclaimed job id: {:?}",
job_id
);
}
}
}
eprint_step!(
"Assert that mailbox_by_path(\"inbox\") does not return a valid result (there are no \
mailboxes)..."
);
assert_eq!(
account.mailbox_by_path("inbox").unwrap_err().to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"You can inspect the list of mailbox paths of an account with the \
manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
eprint_step!(
"Create multiple maildir folders \"inbox/Archive{{1,2,3,4,5,6,7,8,9,10}}\"..."
);
macro_rules! wait_for_job {
($job_id:expr) => {{
let wait_for = $job_id;
while let Ok(thread_event) = ctx.receiver.recv() {
if let crate::ThreadEvent::JobFinished(job_id) = thread_event {
if !account.process_event(&job_id) {
assert!(
ctx.accounts[0].process_event(&job_id),
"unclaimed job id: {:?}",
job_id
);
} else if job_id == wait_for {
break;
}
}
}
}};
}
for i in 1..=10 {
wait_for_job!(account
.mailbox_operation(MailboxOperation::Create(format!("inbox/Archive{i}")))
.unwrap());
}
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"Archive{{n}}\") works, and that we don't have to \
specify the root prefix \"inbox\"..."
);
for i in 1..=10 {
account.mailbox_by_path(&format!("Archive{i}")).unwrap();
}
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"rchive\") returns an error message with matches..."
);
assert_eq!(
account.mailbox_by_path("rchive").unwrap_err().to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"Some matching paths that were found: [\"Archive1\", \"Archive2\", \
\"Archive3\", \"Archive4\", \"Archive5\"] and 5 others. You can inspect the \
list of mailbox paths of an account with the manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
eprint_step!(
"Assert that mailbox_by_path(\"inbox/Archive{{n}}\") does not return a valid result..."
);
assert_eq!(
account
.mailbox_by_path("inbox/Archive1")
.unwrap_err()
.to_string(),
Error {
summary: "Mailbox with that path not found.".into(),
details: Some(
"You can inspect the list of mailbox paths of an account with the \
manage-mailboxes command."
.into()
),
source: None,
inner: None,
related_path: None,
kind: ErrorKind::NotFound
}
.to_string()
);
eprintln_ok();
}
}

View file

@ -21,7 +21,28 @@
//! Command line arguments.
use std::{ffi::OsStr, os::unix::ffi::OsStrExt};
use super::*;
#[cfg(feature = "cli-docs")]
use crate::manpages;
fn try_path_or_stdio(input: &OsStr) -> PathOrStdio {
if input.as_bytes() == b"-" {
PathOrStdio::Stdio
} else {
PathOrStdio::Path(PathBuf::from(input))
}
}
/// `Pathbuf` or standard stream (`-` operand).
#[derive(Debug)]
pub enum PathOrStdio {
/// Path
Path(PathBuf),
/// standard stream (`-` operand)
Stdio,
}
#[derive(Debug, StructOpt)]
#[structopt(name = "meli", about = "terminal mail client", version_short = "v")]
@ -40,39 +61,50 @@ pub enum SubCommand {
PrintDefaultTheme,
/// print loaded themes in full to stdout and exit.
PrintLoadedThemes,
/// print all paths that meli creates/uses.
PrintUsedPaths,
/// print all directories that meli creates/uses.
PrintAppDirectories,
/// print location of configuration file that will be loaded on normal app
/// startup.
PrintConfigPath,
/// edit configuration files with `$EDITOR`/`$VISUAL`.
EditConfig,
/// create a sample configuration file with available configuration options.
/// If PATH is not specified, meli will try to create it in
/// $XDG_CONFIG_HOME/meli/config.toml
/// If `PATH` is not specified, meli will try to create it in
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will output to standard
/// output instead.
#[structopt(display_order = 1)]
CreateConfig {
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
#[structopt(value_name = "NEW_CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
path: Option<PathOrStdio>,
},
/// test a configuration file for syntax issues or missing options.
/// If `PATH` is not specified, meli will try to read it from
/// `$XDG_CONFIG_HOME/meli/config.toml`. Path `-` will read input from
/// standard input instead.
#[structopt(display_order = 2)]
TestConfig {
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str))]
path: Option<PathBuf>,
#[structopt(value_name = "CONFIG_PATH", parse(from_os_str = try_path_or_stdio))]
path: Option<PathOrStdio>,
},
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
#[structopt(display_order = 3)]
/// Testing tools such as IMAP, SMTP shells for debugging.
Tools(ToolOpt),
#[structopt(visible_alias="docs", aliases=&["docs", "manpage", "manpages"])]
#[structopt(display_order = 4)]
/// print documentation page and exit (Piping to a pager is recommended.).
Man(ManOpt),
#[structopt(display_order = 4)]
/// Install manual pages to the first location provided by $MANPATH /
/// manpath(1), unless you specify the directory as an argument.
#[structopt(display_order = 5)]
/// Install manual pages to the first location provided by `$MANPATH` /
/// `manpath(1)`, unless you specify the directory as an argument.
InstallMan {
#[structopt(value_name = "DESTINATION_PATH", parse(from_os_str))]
destination_path: Option<PathBuf>,
},
#[structopt(display_order = 5)]
/// print compile time feature flags of this binary
#[structopt(display_order = 6)]
/// Print compile time feature flags of this binary
CompiledWith,
/// Print log file location.
PrintLogPath,
/// View mail from input file.
View {
#[structopt(value_name = "INPUT", parse(from_os_str))]
@ -82,126 +114,156 @@ pub enum SubCommand {
#[derive(Debug, StructOpt)]
pub struct ManOpt {
#[structopt(default_value = "meli", possible_values=&["meli", "conf", "themes", "meli.7", "guide"], value_name="PAGE", parse(try_from_str = manpages::parse_manpage))]
/// If set, output text in stdout instead of spawning `$PAGER`.
#[cfg(feature = "cli-docs")]
#[cfg_attr(feature = "cli-docs", structopt(long = "no-raw", alias = "no-raw"))]
pub no_raw: bool,
/// If set, output compressed gzip manpage in binary form in stdout.
#[cfg(feature = "cli-docs")]
#[cfg_attr(feature = "cli-docs", structopt(long = "gzipped"))]
pub gzipped: bool,
#[cfg(feature = "cli-docs")]
#[cfg_attr(feature = "cli-docs", structopt(default_value = "meli", possible_values=manpages::POSSIBLE_VALUES, value_name="PAGE", parse(try_from_str = manpages::parse_manpage)))]
/// Name of manual page.
pub page: manpages::ManPages,
/// If true, output text in stdout instead of spawning $PAGER.
#[structopt(long = "no-raw", alias = "no-raw", value_name = "bool")]
#[cfg(feature = "cli-docs")]
pub no_raw: Option<Option<bool>>,
}
#[cfg(feature = "cli-docs")]
pub mod manpages {
use std::{
env, fs,
path::{Path, PathBuf},
sync::Arc,
};
#[derive(Debug, StructOpt)]
pub enum ToolOpt {
ImapShell {
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
account: String,
},
#[cfg(feature = "smtp")]
SmtpShell {
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
account: String,
},
#[cfg(feature = "jmap")]
JmapShell {
#[structopt(value_name = "CONFIG_TOML_ACCOUNT_NAME")]
account: String,
},
}
use melib::log;
use crate::{Error, Result};
pub fn parse_manpage(src: &str) -> Result<ManPages> {
match src {
"" | "meli" | "meli.1" | "main" => Ok(ManPages::Main),
"meli.7" | "guide" => Ok(ManPages::Guide),
"meli.conf" | "meli.conf.5" | "conf" | "config" | "configuration" => Ok(ManPages::Conf),
"meli-themes" | "meli-themes.5" | "themes" | "theming" | "theme" => {
Ok(ManPages::Themes)
}
_ => Err(Error::new(format!("Invalid documentation page: {src}",))),
}
}
#[derive(Copy, Clone, Debug)]
/// Choose manpage
pub enum ManPages {
/// meli(1)
Main = 0,
/// meli.conf(5)
Conf = 1,
/// meli-themes(5)
Themes = 2,
/// meli(7)
Guide = 3,
}
impl std::fmt::Display for ManPages {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
fmt,
"{}",
match self {
Self::Main => "meli.1",
Self::Conf => "meli.conf.5",
Self::Themes => "meli-themes.5",
Self::Guide => "meli.7",
}
fn print_path(path: &std::path::Path) {
if let Some(hostname) = nix::unistd::gethostname()
.ok()
.and_then(|s| s.into_string().ok())
{
println!(
"{}",
Hyperlink::new(
&path.display(),
&format_args!("file://{hostname}{}", path.display())
)
}
}
impl ManPages {
pub fn install(destination: Option<PathBuf>) -> Result<PathBuf> {
fn path_valid(p: &Path, tries: &mut Vec<PathBuf>) -> bool {
tries.push(p.into());
p.exists()
&& p.is_dir()
&& fs::metadata(p)
.ok()
.map(|m| !m.permissions().readonly())
.unwrap_or(false)
}
let mut tries = vec![];
let Some(mut path) = destination
.filter(|p| path_valid(p, &mut tries))
.or_else(|| {
if let Some(paths) = env::var_os("MANPATH") {
if let Some(path) =
env::split_paths(&paths).find(|p| path_valid(p, &mut tries))
{
return Some(path);
}
}
None
})
.or_else(|| {
#[allow(deprecated)]
env::home_dir()
.map(|p| p.join("local").join("share"))
.filter(|p| path_valid(p, &mut tries))
})
else {
return Err(format!("Could not write to any of these paths: {:?}", tries).into());
};
for (p, dir) in [
(ManPages::Main, "man1"),
(ManPages::Conf, "man5"),
(ManPages::Themes, "man5"),
(ManPages::Guide, "man7"),
] {
let text = crate::subcommands::man(p, true)?;
path.push(dir);
std::fs::create_dir_all(&path).map_err(|err| {
Error::new(format!("Could not create {} directory.", path.display()))
.set_source(Some(Arc::new(err)))
})?;
path.push(&p.to_string());
fs::write(&path, text.as_bytes()).map_err(|err| {
Error::new(format!("Could not write to {}", path.display()))
.set_source(Some(Arc::new(err)))
})?;
log::trace!("Installed {} to {}", p, path.display());
path.pop();
path.pop();
}
Ok(path)
}
);
} else {
println!("{}", path.display());
}
}
impl Opt {
/// Execute `self.subcommand` if any, and return its result. Otherwise
/// return `None`.
pub fn execute(self) -> Option<Result<()>> {
macro_rules! ret_err {
($sth:expr) => {
match $sth {
Ok(v) => v,
Err(err) => return Some(Err(err.into())),
}
};
}
Some(match self.subcommand? {
SubCommand::View { .. } => { return None ; }
SubCommand::TestConfig { path } => {
subcommands::test_config(path)
}
SubCommand::Tools(toolopt) => {
subcommands::tool(self.config, toolopt)
}
SubCommand::CreateConfig { path } => {
subcommands::create_config(path)
}
SubCommand::EditConfig => {
subcommands::edit_config()
}
SubCommand::PrintConfigPath => {
let config_path = ret_err!(crate::conf::get_config_file());
print_path(&config_path);
Ok(())
}
#[cfg(not(feature = "cli-docs"))]
SubCommand::Man(ManOpt {}) => {
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
}
#[cfg(feature = "cli-docs")]
SubCommand::Man(ManOpt {
page,
no_raw,
gzipped: true,
}) => {
use std::io::Write;
ret_err!(std::io::stdout().write_all(if no_raw {
page.text_gz()
} else {
page.mdoc_gz()
}));
Ok(())
}
#[cfg(feature = "cli-docs")]
SubCommand::Man(ManOpt {
page,
no_raw,
gzipped: false,
}) => {
subcommands::man(page, false).and_then(|s| subcommands::pager(s, no_raw))
}
SubCommand::CompiledWith => {
subcommands::compiled_with()
}
SubCommand::PrintLoadedThemes => {
let s = ret_err!(conf::FileSettings::new());
print!("{}", s.terminal.themes);
Ok(())
}
SubCommand::PrintDefaultTheme => {
print!("{}", conf::Themes::default().key_to_string("dark", false));
Ok(())
}
SubCommand::PrintAppDirectories => {
print_path(&xdg::BaseDirectories::with_prefix("meli")
.expect(
"Could not find your XDG directories. If this is unexpected, please \
report it as a bug."
)
.get_data_file(""));
let mut temp_dir = std::env::temp_dir();
temp_dir.push("meli");
print_path(&temp_dir);
Ok(())
}
#[cfg(not(feature = "cli-docs"))]
SubCommand::InstallMan {
destination_path: _,
} => {
Err(Error::new("error: this version of meli was not build with embedded documentation (cargo feature `cli-docs`). You might have it installed as manpages (eg `man meli`), otherwise check https://meli-email.org"))
}
#[cfg(feature = "cli-docs")]
SubCommand::InstallMan { destination_path } => {
match crate::manpages::ManPages::install(destination_path) {
Ok(p) => println!("Installed at {}.", p.display()),
Err(err) => return Some(Err(err)),
}
Ok(())
}
SubCommand::PrintLogPath => {
let settings = ret_err!(crate::conf::Settings::new());
print_path(&settings._logger.log_dest());
Ok(())
}
})
}
}

View file

@ -40,6 +40,9 @@ use melib::{
SortField, SortOrder,
};
#[cfg(test)]
mod tests;
pub mod actions;
#[macro_use]
pub mod error;
@ -55,14 +58,15 @@ pub use crate::actions::{
AccountAction::{self, *},
Action::{self, *},
ComposeAction::{self, *},
ComposerTabAction, FlagAction,
ListingAction::{self, *},
MailingListAction::{self, *},
TabAction::{self, *},
TagAction::{self, *},
TagAction,
ViewAction::{self, *},
};
/// Helper macro to convert an array of tokens into a TokenStream
/// Helper macro to convert an array of tokens into a `TokenStream`
macro_rules! to_stream {
($token: expr) => {
TokenStream {
@ -111,7 +115,7 @@ pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> {
}
}
#[derive(Debug, Copy, Clone)]
#[derive(Clone, Copy, Debug)]
pub struct TokenStream {
tokens: &'static [TokenAdicity],
}
@ -142,6 +146,11 @@ impl TokenStream {
tokens.append(&mut m);
}
}
AlternativeStrings(v) => {
for t in v.iter() {
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, t));
}
}
Seq(_s) => {}
RestOfStringValue => {
sugg.insert(String::new());
@ -195,6 +204,20 @@ impl TokenStream {
*s = "";
}
}
AlternativeStrings(v) => {
for lit in v.iter() {
if lit.starts_with(*s) && lit.len() != s.len() {
sugg.insert(lit[s.len()..].to_string());
tokens.push((s, *t.inner()));
return tokens;
} else if s.starts_with(lit) {
tokens.push((&s[..lit.len()], *t.inner()));
*s = &s[lit.len()..];
} else {
return vec![];
}
}
}
Seq(_s) => {
return vec![];
}
@ -225,7 +248,7 @@ impl TokenStream {
/// `Token` wrapper that defines how many times a token is expected to be
/// repeated
#[derive(Debug, Copy, Clone)]
#[derive(Clone, Copy, Debug)]
pub enum TokenAdicity {
ZeroOrOne(Token),
ZeroOrMore(Token),
@ -245,11 +268,12 @@ impl TokenAdicity {
}
/// A token encountered in the UI's command execution bar
#[derive(Debug, Copy, Clone)]
#[derive(Clone, Copy, Debug)]
pub enum Token {
Literal(&'static str),
Filepath,
Alternatives(&'static [TokenStream]),
AlternativeStrings(&'static [&'static str]),
Seq(&'static [TokenAdicity]),
AccountName,
MailboxPath,
@ -339,6 +363,11 @@ define_commands!([
tokens: &[One(Literal("search")), One(RestOfStringValue)],
parser: parser::search
},
{ tags: ["clear-selection"],
desc: "clear-selection",
tokens: &[One(Literal("clear-selection"))],
parser: parser::select
},
{ tags: ["select"],
desc: "select <TERM>, selects envelopes matching with given term",
tokens: &[One(Literal("select")), One(RestOfStringValue)],
@ -397,6 +426,11 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
tokens: &[One(Literal("save-draft"))],
parser: parser::save_draft
},
{ tags: ["discard-draft"],
desc: "discard draft",
tokens: &[One(Literal("discard-draft"))],
parser: parser::discard_draft
},
{ tags: ["toggle sign "],
desc: "switch between sign/unsign for this draft",
tokens: &[One(Literal("toggle")), One(Literal("sign"))],
@ -482,6 +516,18 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
tokens: &[One(Literal("manage-mailboxes"))],
parser: parser::manage_mailboxes
},
{ tags: ["man"],
desc: "read documentation",
tokens: {
#[cfg(feature = "cli-docs")]
{
&[One(Literal("man")), One(AlternativeStrings(crate::manpages::POSSIBLE_VALUES))]
}
#[cfg(not(feature = "cli-docs"))]
{ &[] }
},
parser: parser::view_manpage
},
{ tags: ["manage-jobs"],
desc: "view and manage jobs",
tokens: &[One(Literal("manage-jobs"))],
@ -502,7 +548,7 @@ Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_str
/// Get command suggestions for input
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
use crate::melib::ShellExpandTrait;
let mut sugg = Default::default();
let mut sugg: HashSet<String> = Default::default();
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
let _m = tokens.matches(&mut &(*input), &mut sugg);
if _m.is_empty() {
@ -510,175 +556,10 @@ pub fn command_completion_suggestions(input: &str) -> Vec<String> {
}
if let Some((s, Filepath)) = _m.last() {
let p = std::path::Path::new(s);
sugg.extend(p.complete(true).into_iter());
sugg.extend(p.complete(true, s.ends_with('/')).into_iter());
}
}
sugg.into_iter()
.map(|s| format!("{}{}", input, s.as_str()))
.collect::<Vec<String>>()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_command_parser() {
let mut input = "sort".to_string();
macro_rules! match_input {
($input:expr) => {{
let mut sugg = Default::default();
let mut vec = vec![];
//print!("{}", $input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let m = tokens.matches(&mut $input.as_str(), &mut sugg);
if !m.is_empty() {
vec.push(tokens);
//print!("{:?} ", desc);
//println!(" result = {:#?}\n\n", m);
}
}
//println!("suggestions = {:#?}", sugg);
sugg.into_iter()
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
.collect::<HashSet<String>>()
}};
}
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()])
.collect(),
);
input = "so".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["sort".to_string()]).collect(),
);
input = "so ".to_string();
assert_eq!(&match_input!(input), &HashSet::default(),);
input = "to".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
);
input = "toggle ".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter([
"toggle mouse".to_string(),
"toggle sign".to_string(),
"toggle encrypt".to_string(),
"toggle thread_snooze".to_string()
])
.collect(),
);
}
#[test]
#[ignore]
fn test_parser_interactive() {
use std::io;
let mut input = String::new();
loop {
input.clear();
print!("> ");
match io::stdin().read_line(&mut input) {
Ok(_n) => {
println!("Input is {:?}", input.as_str().trim());
let mut sugg = Default::default();
let mut vec = vec![];
//print!("{}", input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
if !m.is_empty() {
vec.push(tokens);
//print!("{:?} ", desc);
//println!(" result = {:#?}\n\n", m);
}
}
println!(
"suggestions = {:#?}",
sugg.into_iter()
.zip(vec.into_iter())
.map(|(s, v)| format!(
"{}{} {:?}",
input.as_str().trim(),
if input.trim().is_empty() {
s.trim()
} else {
s.as_str()
},
v
))
.collect::<Vec<String>>()
);
if input.trim() == "quit" {
break;
}
}
Err(error) => println!("error: {}", error),
}
}
println!("alright");
}
#[test]
fn test_command_parser_all() {
use CommandError::*;
for cmd in [
"set unseen",
"set seen",
"delete",
"copyto somewhere",
"moveto somewhere",
"import fpath mpath",
"close ",
"go 5",
] {
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
}
assert_eq!(
parse_command(b"setfafsfoo").unwrap_err().to_string(),
Parsing {
inner: "setfafsfoo".into(),
kind: "".into(),
}
.to_string(),
);
assert_eq!(
parse_command(b"set foo").unwrap_err().to_string(),
BadValue {
inner: "Bad argument for `set`. Accepted arguments are [seen, unseen, plain, \
threaded, compact, conversations]."
.into(),
}
.to_string(),
);
assert_eq!(
parse_command(b"moveto ").unwrap_err().to_string(),
WrongNumberOfArguments {
too_many: false,
takes: (1, Some(1)),
given: 0,
__func__: "moveto",
inner: "".into(),
}
.to_string(),
);
assert_eq!(
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
WrongNumberOfArguments {
too_many: true,
takes: (1, Some(1)),
given: 2,
__func__: "reindex",
inner: "".into(),
}
.to_string(),
);
}
}

View file

@ -21,19 +21,25 @@
//! User actions that need to be handled by the UI
use std::path::PathBuf;
use std::{path::PathBuf, sync::Arc};
use melib::{email::mailto::Mailto, SortField, SortOrder};
use melib::{email::mailto::Mailto, Flag, SortField, SortOrder};
use crate::components::{Component, ComponentId};
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum FlagAction {
Set(Flag),
Unset(Flag),
}
#[derive(Debug, Eq, PartialEq)]
pub enum TagAction {
Add(String),
Remove(String),
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum ListingAction {
SetPlain,
SetThreaded,
@ -43,6 +49,7 @@ pub enum ListingAction {
Select(String),
SetSeen,
SetUnseen,
SendToTrash,
CopyTo(MailboxPath),
CopyToOtherAccount(AccountName, MailboxPath),
MoveTo(MailboxPath),
@ -52,53 +59,66 @@ pub enum ListingAction {
Delete,
OpenInNewTab,
Tag(TagAction),
Flag(FlagAction),
ClearSelection,
ToggleThreadSnooze,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum ComposerTabAction {
DiscardDraft,
SaveDraft,
#[cfg(feature = "gpgme")]
ToggleSign,
#[cfg(feature = "gpgme")]
ToggleEncrypt,
AddAttachment(String),
AddAttachmentFilePicker(Option<String>),
AddAttachmentPipe(String),
RemoveAttachment(usize),
}
#[derive(Debug, PartialEq)]
pub enum TabAction {
ComposerAction(ComposerTabAction),
Close,
Kill(ComponentId),
New(Option<Box<dyn Component>>),
ManageMailboxes,
ManageJobs,
#[cfg(feature = "cli-docs")]
Man(crate::manpages::ManPages),
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum MailingListAction {
ListPost,
ListArchive,
ListUnsubscribe,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum ViewAction {
Pipe(String, Vec<String>),
Filter(String),
Filter(Option<String>),
SaveAttachment(usize, String),
PipeAttachment(usize, String, Vec<String>),
ExportMail(String),
AddAddressesToContacts,
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum ComposeAction {
AddAttachment(String),
AddAttachmentFilePicker(Option<String>),
AddAttachmentPipe(String),
RemoveAttachment(usize),
SaveDraft,
ToggleSign,
ToggleEncrypt,
Mailto(Mailto),
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum AccountAction {
ReIndex,
PrintAccountSetting(String),
}
#[derive(Debug)]
#[derive(Debug, Eq, PartialEq)]
pub enum MailboxOperation {
Create(NewMailboxPath),
Delete(MailboxPath),
@ -109,7 +129,7 @@ pub enum MailboxOperation {
SetPermissions(MailboxPath),
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub enum Action {
Listing(ListingAction),
ViewMailbox(usize),
@ -136,10 +156,10 @@ impl Action {
pub fn needs_confirmation(&self) -> bool {
matches!(
self,
Action::Listing(ListingAction::Delete)
| Action::MailingListAction(_)
| Action::Mailbox(_, _)
| Action::Quit
Self::Listing(ListingAction::Delete)
| Self::MailingListAction(_)
| Self::Mailbox(_, _)
| Self::Quit
)
}
}
@ -149,7 +169,7 @@ type MailboxPath = String;
type NewMailboxPath = String;
macro_rules! impl_into_action {
($({$t:ty => $var:tt}),*) => {
($({$t:ty => $var:tt}),*$(,)?) => {
$(
impl From<$t> for Action {
fn from(v: $t) -> Self {
@ -160,11 +180,11 @@ macro_rules! impl_into_action {
};
}
macro_rules! impl_tuple_into_action {
($({$a:ty,$b:ty => $var:tt}),*) => {
($({$a:ty,$b:ty => $var:tt}),*$(,)?) => {
$(
impl From<($a,$b)> for Action {
fn from((a, b): ($a,$b)) -> Self {
Self::$var(a, b)
Self::$var(a.to_string(), b)
}
}
)*
@ -180,5 +200,7 @@ impl_into_action!(
);
impl_tuple_into_action!(
{ AccountName, MailboxOperation => Mailbox },
{ AccountName, AccountAction => AccountAction }
{ AccountName, AccountAction => AccountAction },
{ Arc<str>, MailboxOperation => Mailbox },
{ Arc<str>, AccountAction => AccountAction },
);

View file

@ -21,7 +21,7 @@
use super::*;
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub enum CommandError {
Parsing {
inner: Cow<'static, str>,
@ -29,6 +29,7 @@ pub enum CommandError {
},
BadValue {
inner: Cow<'static, str>,
suggestions: Option<&'static [&'static str]>,
},
WrongNumberOfArguments {
too_many: bool,
@ -63,7 +64,25 @@ impl std::fmt::Display for CommandError {
Self::Parsing { inner, kind: _ } => {
write!(fmt, "Could not parse command: {}", inner)
}
Self::BadValue { inner } => {
Self::BadValue {
inner,
suggestions: Some(suggs),
} => {
write!(fmt, "Bad value/argument: {}. Possible values are: ", inner)?;
let len = suggs.len();
for (i, val) in suggs.iter().enumerate() {
if i == len.saturating_sub(1) {
write!(fmt, "{}", val)?;
} else {
write!(fmt, "{}, ", val)?;
}
}
write!(fmt, "")
}
Self::BadValue {
inner,
suggestions: None,
} => {
write!(fmt, "Bad value/argument: {}", inner)
}
Self::WrongNumberOfArguments {

View file

@ -24,22 +24,37 @@
use super::*;
use crate::command::{argcheck::*, error::*};
const FLAG_SUGGESTIONS: &[&str] = &[
"passed",
"replied",
"seen or read",
"junk or trash or trashed",
"draft",
"flagged",
];
macro_rules! command_err {
(nom $b:expr, $input: expr, $msg:literal) => {{
(nom $b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
let evaluated: IResult<&'_ [u8], _> = { $b };
match evaluated {
Err(_) => {
let err = CommandError::BadValue { inner: $msg.into() };
let err = CommandError::BadValue {
inner: $msg.into(),
suggestions: $suggs,
};
return Ok(($input, Err(err)));
}
Ok(v) => v,
}
}};
($b:expr, $input: expr, $msg:literal) => {{
($b:expr, $input: expr, $msg:expr, $suggs:expr) => {{
let evaluated = { $b };
match evaluated {
Err(_) => {
let err = CommandError::BadValue { inner: $msg.into() };
let err = CommandError::BadValue {
inner: $msg.into(),
suggestions: $suggs,
};
return Ok(($input, Err(err)));
}
Ok(v) => v,
@ -101,11 +116,18 @@ pub fn listing_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErro
open_in_new_tab,
export_mbox,
_tag,
flag,
))(input)
}
pub fn compose_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
alt((add_attachment, mailto, remove_attachment, save_draft))(input)
alt((
add_attachment,
mailto,
remove_attachment,
save_draft,
discard_draft,
))(input)
}
pub fn account_action(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
@ -117,13 +139,14 @@ pub fn view(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
filter,
pipe,
save_attachment,
pipe_attachment,
export_mail,
add_addresses_to_contacts,
))(input)
}
pub fn new_tab(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
alt((manage_mailboxes, manage_jobs, compose_action))(input)
alt((manage_mailboxes, manage_jobs, compose_action, view_manpage))(input)
}
pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
@ -154,6 +177,125 @@ pub fn parse_command(input: &[u8]) -> Result<Action, CommandError> {
.and_then(|(_, v)| v)
}
/// Set/unset a flag.
///
/// # Example
///
/// ```
/// # use meli::{melib::Flag, command::{Action,ListingAction, FlagAction, parser}};
///
/// let (rest, parsed) = parser::flag(b"flag set junk").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
/// Flag::TRASHED
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag unset junk").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Unset(
/// Flag::TRASHED
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag set draft").unwrap();
/// assert_eq!(rest, b"");
/// assert!(
/// matches!(
/// parsed,
/// Ok(Action::Listing(ListingAction::Flag(FlagAction::Set(
/// Flag::DRAFT
/// ))))
/// ),
/// "{:?}",
/// parsed
/// );
///
/// let (rest, parsed) = parser::flag(b"flag set xunk").unwrap();
/// assert_eq!(rest, b"");
/// assert_eq!(
/// &parsed.unwrap_err().to_string(),
/// "Bad value/argument: xunk is not a valid flag name. Possible values are: passed, replied, \
/// seen or read, junk or trash or trashed, draft, flagged"
/// );
/// ```
pub fn flag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
use melib::Flag;
fn parse_flag(s: &str) -> Option<Flag> {
match s {
o if o.eq_ignore_ascii_case("passed") => Some(Flag::PASSED),
o if o.eq_ignore_ascii_case("replied") => Some(Flag::REPLIED),
o if o.eq_ignore_ascii_case("seen") => Some(Flag::SEEN),
o if o.eq_ignore_ascii_case("read") => Some(Flag::SEEN),
o if o.eq_ignore_ascii_case("junk") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("trash") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("trashed") => Some(Flag::TRASHED),
o if o.eq_ignore_ascii_case("draft") => Some(Flag::DRAFT),
o if o.eq_ignore_ascii_case("flagged") => Some(Flag::FLAGGED),
_ => None,
}
}
preceded(
tag("flag"),
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
let (input, _) = tag("set")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, flag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
let Some(flag) = parse_flag(flag) else {
return Ok((
b"",
Err(CommandError::BadValue {
inner: format!("{flag} is not a valid flag name").into(),
suggestions: Some(FLAG_SUGGESTIONS),
}),
));
};
Ok((input, Ok(Listing(Flag(FlagAction::Set(flag))))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, flag};
let (input, _) = tag("unset")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, flag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
let Some(flag) = parse_flag(flag) else {
return Ok((
b"",
Err(CommandError::BadValue {
inner: format!("{flag} is not a valid flag name").into(),
suggestions: Some(FLAG_SUGGESTIONS),
}),
));
};
Ok((input, Ok(Listing(Flag(FlagAction::Unset(flag))))))
},
)),
)(input.trim())
}
pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, set};
@ -173,9 +315,13 @@ pub fn set(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, ret) = command_err!(nom
alt((map(tag("seen"), |_| Listing(SetSeen)), map(tag("unseen"), |_| Listing(SetUnseen))))(input),
alt((
map(tag("seen"), |_| Listing(SetSeen)),
map(tag("unseen"), |_| Listing(SetUnseen)
)))(input),
input,
"Bad argument for `set`. Accepted arguments are [seen, unseen, plain, threaded, compact, conversations].");
String::from_utf8_lossy(input.trim()).to_string(),
Some(&["seen", "unseen", "plain", "threaded", "compact", "conversations"]));
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(ret)))
@ -280,7 +426,8 @@ pub fn goto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, nth) = command_err!(nom
usize_c(input),
input,
"Argument must be an integer.");
"Argument must be an integer.",
None);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Action::ViewMailbox(nth))))
@ -336,6 +483,26 @@ pub fn search(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
Ok((input, Ok(Listing(Search(String::from(string))))))
}
pub fn select(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
#[inline]
fn clear_selection(input: &[u8]) -> Option<IResult<&[u8], Result<Action, CommandError>>> {
if !input.trim().starts_with(b"clear-selection") {
return None;
}
#[inline]
fn inner(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, clear_selection};
let (input, _) = tag("clear-selection")(input.ltrim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(ListingAction::ClearSelection))))
}
Some(inner(input))
}
if let Some(retval) = clear_selection(input) {
return retval;
}
let mut check = arg_init! { min_arg:1, max_arg: {u8::MAX}, select};
let (input, _) = tag("select")(input.trim())?;
arg_chk!(start check, input);
@ -402,15 +569,15 @@ pub fn printenv(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
Ok((input, Ok(PrintEnv(key.to_string()))))
}
pub fn currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, currentdir};
let (input, _) = tag("cwd")(input.ltrim())?;
let mut check = arg_init! { min_arg:0, max_arg: 0, pwd};
let (input, _) = alt((tag("cwd"), tag("pwd")))(input.ltrim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(CurrentDirectory)))
}
pub fn change_currentdir(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, change_currentdir};
let mut check = arg_init! { min_arg: 1, max_arg: 1, cd};
let (input, _) = tag("cd")(input.ltrim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
@ -433,7 +600,8 @@ pub fn mailto(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let (input, val) = command_err!(
parser(val.as_bytes()),
val.as_bytes(),
"Could not parse mailto value. If the value is valid, please report this bug."
"Could not parse mailto value. If the value is valid, please report this bug.",
None
);
Ok((input, Ok(Compose(Mailto(val)))))
}
@ -473,14 +641,18 @@ pub fn pipe<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
))(input)
}
pub fn filter(input: &'_ [u8]) -> IResult<&'_ [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, filter};
let mut check = arg_init! { min_arg:0, max_arg:255, filter};
let (input, _) = tag("filter")(input.trim())?;
arg_chk!(start check, input);
if let Ok((input, _)) = eof(input) {
arg_chk!(finish check, input);
return Ok((input, Ok(View(Filter(None)))));
}
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, cmd) = map_res(not_line_ending, std::str::from_utf8)(input)?;
arg_chk!(finish check, input);
Ok((input, Ok(View(Filter(cmd.to_string())))))
Ok((input, Ok(View(Filter(Some(cmd.to_string()))))))
}
pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
alt((
@ -495,7 +667,12 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
let (input, cmd) = quoted_argument(input)?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Compose(AddAttachmentPipe(cmd.to_string())))))
Ok((
input,
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachmentPipe(
cmd.to_string(),
)))),
))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment};
@ -506,32 +683,46 @@ pub fn add_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, C
let (input, path) = quoted_argument(input)?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Compose(AddAttachment(path.to_string())))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Compose(AddAttachmentFilePicker(None)))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("<")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((
input,
Ok(Compose(AddAttachmentFilePicker(Some(shell.to_string())))),
Ok(Tab(ComposerAction(ComposerTabAction::AddAttachment(
path.to_string(),
)))),
))
},
alt((
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, add_attachment_file_picker};
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
let (input, _) = tag("<")(input.trim())?;
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, shell) = map_res(not_line_ending, std::str::from_utf8)(input)?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((
input,
Ok(Tab(ComposerAction(
ComposerTabAction::AddAttachmentFilePicker(Some(shell.to_string())),
))),
))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, add_attachment};
let (input, _) = tag("add-attachment-file-picker")(input.trim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((
input,
Ok(Tab(ComposerAction(
ComposerTabAction::AddAttachmentFilePicker(None),
))),
))
},
)),
))(input)
}
pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
@ -543,7 +734,12 @@ pub fn remove_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandE
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Compose(RemoveAttachment(idx)))))
Ok((
input,
Ok(Tab(ComposerAction(ComposerTabAction::RemoveAttachment(
idx,
)))),
))
}
pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, save_draft };
@ -551,7 +747,18 @@ pub fn save_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Compose(SaveDraft))))
Ok((input, Ok(Tab(ComposerAction(ComposerTabAction::SaveDraft)))))
}
pub fn discard_draft(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, discard_draft };
let (input, _) = tag("discard-draft")(input.trim())?;
arg_chk!(start check, input);
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((
input,
Ok(Tab(ComposerAction(ComposerTabAction::DiscardDraft))),
))
}
pub fn create_mailbox(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, create_malbox};
@ -689,6 +896,35 @@ pub fn save_attachment(input: &[u8]) -> IResult<&[u8], Result<Action, CommandErr
let (input, _) = eof(input)?;
Ok((input, Ok(View(SaveAttachment(idx, path.to_string())))))
}
pub fn pipe_attachment<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg:{u8::MAX}, pipe_attachment};
let (input, _) = tag("pipe-attachment")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, idx) = map_res(quoted_argument, usize::from_str)(input)?;
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
let (input, bin) = quoted_argument(input)?;
arg_chk!(inc check, input);
let (input, args) = alt((
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
let (input, _) = is_a(" ")(input)?;
let (input, args) = separated_list1(is_a(" "), quoted_argument)(input)?;
let (input, _) = eof(input)?;
Ok((
input,
args.into_iter().map(String::from).collect::<Vec<String>>(),
))
},
|input: &'a [u8]| -> IResult<&'a [u8], Vec<String>> {
let (input, _) = eof(input)?;
Ok((input, Vec::with_capacity(0)))
},
))(input)?;
arg_chk!(finish check, input);
Ok((input, Ok(View(PipeAttachment(idx, bin.to_string(), args)))))
}
pub fn export_mail(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, export_mail};
let (input, _) = tag("export-mail")(input.trim())?;
@ -708,6 +944,19 @@ pub fn add_addresses_to_contacts(input: &[u8]) -> IResult<&[u8], Result<Action,
let (input, _) = eof(input)?;
Ok((input, Ok(View(AddAddressesToContacts))))
}
/// Set/unset a tag.
///
/// # Example
///
/// ```
/// # use meli::command::{Action,ListingAction, TagAction, parser::_tag};
///
/// let (rest, parsed) = _tag(b"tag add newsletters").unwrap();
/// println!("parsed is {:?}", parsed);
/// assert_eq!(rest, b"");
/// assert!(matches!(parsed, Ok(Action::Listing(ListingAction::Tag(TagAction::Add(ref tagname)))) if tagname == "newsletters"), "{:?}", parsed);
/// ```
pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandError>> {
preceded(
tag("tag"),
@ -721,7 +970,7 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
let (input, tag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(Tag(Add(tag.to_string()))))))
Ok((input, Ok(Listing(Tag(TagAction::Add(tag.to_string()))))))
},
|input: &'a [u8]| -> IResult<&'a [u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, tag};
@ -732,11 +981,12 @@ pub fn _tag<'a>(input: &'a [u8]) -> IResult<&'a [u8], Result<Action, CommandErro
let (input, tag) = quoted_argument(input.trim())?;
arg_chk!(finish check, input);
let (input, _) = eof(input)?;
Ok((input, Ok(Listing(Tag(Remove(tag.to_string()))))))
Ok((input, Ok(Listing(Tag(TagAction::Remove(tag.to_string()))))))
},
)),
)(input.trim())
}
pub fn print_account_setting(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:2, max_arg: 2, print};
let (input, _) = tag("print")(input.trim())?;
@ -782,8 +1032,13 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
for (tok, action) in [
("thread_snooze", Listing(ToggleThreadSnooze)),
("mouse", ToggleMouse),
("sign", Compose(ToggleSign)),
("encrypt", Compose(ToggleEncrypt)),
#[cfg(feature = "gpgme")]
("sign", Tab(ComposerAction(ComposerTabAction::ToggleSign))),
#[cfg(feature = "gpgme")]
(
"encrypt",
Tab(ComposerAction(ComposerTabAction::ToggleEncrypt)),
),
] {
if let Ok((inner_input, _)) = tag!()(tok)(input.trim()) {
input = inner_input;
@ -796,7 +1051,8 @@ pub fn toggle(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
return Ok((
input,
Err(CommandError::BadValue {
inner: "Valid toggle values are thread_snooze, mouse, sign, encrypt.".into(),
inner: String::from_utf8_lossy(input).to_string().into(),
suggestions: Some(&["thread_snooze", "mouse", "sign", "encrypt"]),
}),
));
}
@ -823,6 +1079,41 @@ pub fn manage_jobs(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>>
let (input, _) = eof(input)?;
Ok((input, Ok(Tab(ManageJobs))))
}
pub fn view_manpage(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:1, max_arg: 1, view_manpage };
let (input, _) = tag("man")(input.trim())?;
arg_chk!(start check, input);
let (input, _) = is_a(" ")(input)?;
arg_chk!(inc check, input);
#[allow(unused_variables)]
let (input, manpage) = map_res(not_line_ending, std::str::from_utf8)(input.trim())?;
let (input, _) = eof(input)?;
arg_chk!(finish check, input);
#[cfg(feature = "cli-docs")]
{
match crate::manpages::parse_manpage(manpage) {
Ok(m) => Ok((input, Ok(Tab(Man(m))))),
Err(err) => Ok((
input,
Err(CommandError::BadValue {
inner: err.to_string().into(),
suggestions: Some(crate::manpages::POSSIBLE_VALUES),
}),
)),
}
}
#[cfg(not(feature = "cli-docs"))]
{
Ok((
input,
Err(CommandError::Other {
inner: "this meli binary has not been compiled with the cli-docs feature".into(),
}),
))
}
}
pub fn quit(input: &[u8]) -> IResult<&[u8], Result<Action, CommandError>> {
let mut check = arg_init! { min_arg:0, max_arg: 0, quit};
let (input, _) = tag("quit")(input.trim())?;

206
meli/src/command/tests.rs Normal file
View file

@ -0,0 +1,206 @@
//
// meli
//
// Copyright 2017- Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use super::*;
#[test]
fn test_command_parser() {
let mut input = "sort".to_string();
macro_rules! match_input {
($input:expr) => {{
let mut sugg: HashSet<String> = Default::default();
//print!("{}", $input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
// //println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let _ = tokens.matches(&mut $input.as_str(), &mut sugg);
// if !m.is_empty() {
// //print!("{:?} ", desc);
// //println!(" result = {:#?}\n\n", m);
// }
}
//println!("suggestions = {:#?}", sugg);
sugg.into_iter()
.map(|s| format!("{}{}", $input.as_str(), s.as_str()))
.collect::<HashSet<String>>()
}};
}
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["sort date".to_string(), "sort subject".to_string()]).collect(),
);
input = "so".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["sort".to_string()]).collect(),
);
input = "so ".to_string();
assert_eq!(&match_input!(input), &HashSet::default(),);
input = "to".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter(["toggle".to_string()]).collect(),
);
input = "toggle ".to_string();
assert_eq!(
&match_input!(input),
&IntoIterator::into_iter([
"toggle mouse".to_string(),
"toggle sign".to_string(),
"toggle encrypt".to_string(),
"toggle thread_snooze".to_string()
])
.collect(),
);
}
#[test]
#[ignore]
fn test_command_parser_interactive() {
use std::io;
let mut input = String::new();
loop {
input.clear();
print!("> ");
match io::stdin().read_line(&mut input) {
Ok(_n) => {
println!("Input is {:?}", input.as_str().trim());
let mut sugg: HashSet<String> = Default::default();
let mut vec = vec![];
//print!("{}", input);
for (_tags, _desc, tokens, _) in COMMAND_COMPLETION.iter() {
//println!("{:?}, {:?}, {:?}", _tags, _desc, tokens);
let m = tokens.matches(&mut input.as_str().trim(), &mut sugg);
if !m.is_empty() {
vec.push(tokens);
//print!("{:?} ", desc);
//println!(" result = {:#?}\n\n", m);
}
}
println!(
"suggestions = {:#?}",
sugg.into_iter()
.zip(vec.into_iter())
.map(|(s, v)| format!(
"{}{} {:?}",
input.as_str().trim(),
if input.trim().is_empty() {
s.trim()
} else {
s.as_str()
},
v
))
.collect::<Vec<String>>()
);
if input.trim() == "quit" {
break;
}
}
Err(error) => println!("error: {}", error),
}
}
println!("alright");
}
#[test]
fn test_command_parser_all() {
use CommandError::*;
for cmd in [
"set unseen",
"set seen",
"delete",
"copyto somewhere",
"moveto somewhere",
"import fpath mpath",
"close ",
"go 5",
] {
parse_command(cmd.as_bytes()).unwrap_or_else(|err| panic!("{} failed {}", cmd, err));
}
assert_eq!(
parse_command(b"setfafsfoo").unwrap_err().to_string(),
Parsing {
inner: "setfafsfoo".into(),
kind: "".into(),
}
.to_string(),
);
assert_eq!(
parse_command(b"set foo").unwrap_err().to_string(),
BadValue {
inner: "foo".into(),
suggestions: Some(&[
"seen",
"unseen",
"plain",
"threaded",
"compact",
"conversations"
])
}
.to_string(),
);
assert_eq!(
parse_command(b"moveto ").unwrap_err().to_string(),
WrongNumberOfArguments {
too_many: false,
takes: (1, Some(1)),
given: 0,
__func__: "moveto",
inner: "".into(),
}
.to_string(),
);
assert_eq!(
parse_command(b"reindex 1 2 3").unwrap_err().to_string(),
WrongNumberOfArguments {
too_many: true,
takes: (1, Some(1)),
given: 2,
__func__: "reindex",
inner: "".into(),
}
.to_string(),
);
}
#[test]
fn test_command_error_display() {
assert_eq!(
&CommandError::BadValue {
inner: "foo".into(),
suggestions: Some(&[
"seen",
"unseen",
"plain",
"threaded",
"compact",
"conversations"
])
}
.to_string(),
"Bad value/argument: foo. Possible values are: seen, unseen, plain, threaded, compact, \
conversations"
);
}

View file

@ -31,7 +31,7 @@ use uuid::Uuid;
use super::*;
#[derive(Clone, Copy, Eq, Deserialize, Hash, Ord, PartialOrd, PartialEq, Serialize)]
#[derive(Clone, Copy, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[repr(transparent)]
pub struct ComponentId(Uuid);
@ -98,7 +98,7 @@ impl ExtendShortcutsMaps for ShortcutMaps {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum PageMovement {
Up(usize),
Right(usize),
@ -110,14 +110,14 @@ pub enum PageMovement {
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScrollContext {
pub shown_lines: usize,
pub total_lines: usize,
pub has_more_lines: bool,
}
#[derive(Debug, Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub enum ScrollUpdate {
End(ComponentId),
Update {
@ -126,6 +126,8 @@ pub enum ScrollUpdate {
},
}
/// A user interface component (also referred to as a UI widget).
///
/// Types implementing this Trait can draw on the terminal and receive events.
/// If a type wants to skip drawing if it has not changed anything, it can hold
/// some flag in its fields (eg `self.dirty = false`) and act upon that in their
@ -248,6 +250,12 @@ impl Component for Box<dyn Component> {
}
}
impl PartialEq for Box<dyn Component> {
fn eq(&self, other: &Self) -> bool {
self == other || self.id() == other.id()
}
}
bitflags::bitflags! {
/// Attributes of a [`Component`] widget.
///
@ -266,7 +274,7 @@ impl Default for ComponentAttr {
}
}
#[derive(Eq, PartialEq, Debug, Clone)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComponentPath {
pub id: ComponentId,
pub tail: SmallVec<[ComponentId; 8]>,
@ -297,7 +305,7 @@ impl ComponentPath {
// log::trace!("continue;");
continue;
}
cursor = cursor.children().remove(id)?;
cursor = cursor.children().shift_remove(id)?;
}
Some(cursor)
}
@ -312,3 +320,22 @@ impl ComponentPath {
self.tail.last()
}
}
pub mod prelude {
pub use std::borrow::Cow;
pub use indexmap::IndexMap;
pub use crate::{
accounts::MailboxEntry,
command::*,
components::{
Component, ComponentAttr, ComponentId, ComponentPath, ExtendShortcutsMaps,
ScrollContext, *,
},
jobs::{JobId, JobMetadata},
melib::{text::TextProcessing, utils::datetime, SortOrder},
shortcut, AccountHash, Action, Area, Attr, CellBuffer, Context, DataColumns, EnvelopeHash,
Key, MailboxHash, Shortcuts, StatusEvent, ThemeAttribute, UIDialog, UIEvent, UIMode,
};
}

File diff suppressed because it is too large Load diff

View file

@ -20,22 +20,22 @@
*/
//! Configuration for composing email.
use std::collections::HashMap;
use melib::{email::HeaderName, ToggleFlag};
use std::path::PathBuf;
use super::{
default_vals::{ask, false_val, none, true_val},
use indexmap::IndexMap;
use melib::{conf::ActionFlag, email::HeaderName};
use serde::{de, Deserialize, Deserializer};
use crate::conf::{
default_values::{ask, false_val, none, true_val},
deserializers::non_empty_string,
};
/// Settings for writing and sending new e-mail
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ComposingSettings {
/// A command to pipe new emails to
/// Required
pub send_mail: SendMail,
/// Command to launch editor. Can have arguments. Draft filename is given as
/// the last argument. If it's missing, the environment variable $EDITOR is
/// looked up.
@ -61,24 +61,26 @@ pub struct ComposingSettings {
/// Set default header values for new drafts
/// Default: empty
#[serde(default, alias = "default-header-values")]
pub default_header_values: HashMap<HeaderName, String>,
/// Wrap header preample when editing a draft in an editor. This allows you
pub default_header_values: IndexMap<HeaderName, String>,
/// Wrap header preamble when editing a draft in an editor. This allows you
/// to write non-plain text email without the preamble creating syntax
/// errors. They are stripped when you return from the editor. The
/// values should be a two element array of strings, a prefix and suffix.
/// Default: None
#[serde(default, alias = "wrap-header-preample")]
#[serde(default, alias = "wrap-header-preamble")]
pub wrap_header_preamble: Option<(String, String)>,
/// Store sent mail after successful submission. This setting is meant to be
/// disabled for non-standard behaviour in gmail, which auto-saves sent
/// mail on its own. Default: true
#[serde(default = "true_val")]
pub store_sent_mail: bool,
/// The attribution line appears above the quoted reply text.
/// The attribution line that appears above the quoted reply text.
///
/// The format specifiers for the replied address are:
/// - `%+f` — the sender's name and email address.
/// - `%+n` — the sender's name (or email address, if no name is included).
/// - `%+a` — the sender's email address.
///
/// The format string is passed to strftime(3) with the replied envelope's
/// date. Default: "On %a, %0e %b %Y %H:%M, %+f wrote:%n"
#[serde(default = "none")]
@ -91,7 +93,7 @@ pub struct ComposingSettings {
/// Forward emails as attachment? (Alternative is inline)
/// Default: ask
#[serde(default = "ask", alias = "forward-as-attachment")]
pub forward_as_attachment: ToggleFlag,
pub forward_as_attachment: ActionFlag,
/// Alternative lists of reply prefixes (etc. ["Re:", "RE:", ...]) to strip
/// Default: `["Re:", "RE:", "Fwd:", "Fw:", "回复:", "回覆:", "SV:", "Sv:",
/// "VS:", "Antw:", "Doorst:", "VS:", "VL:", "REF:", "TR:", "TR:", "AW:",
@ -110,26 +112,56 @@ pub struct ComposingSettings {
/// Disabled `compose-hooks`.
#[serde(default, alias = "disabled-compose-hooks")]
pub disabled_compose_hooks: Vec<String>,
/// Plain text file with signature that will pre-populate an email draft.
///
/// Signatures must be explicitly enabled to be used, otherwise this setting
/// will be ignored.
///
/// Default: `None`
#[serde(default, alias = "signature-file")]
pub signature_file: Option<PathBuf>,
/// Pre-populate email drafts with signature, if any.
///
/// `meli` will lookup the signature value in this order:
///
/// 1. The `signature_file` setting.
/// 2. `${XDG_CONFIG_DIR}/meli/<account>/signature`
/// 3. `${XDG_CONFIG_DIR}/meli/signature`
/// 4. `${XDG_CONFIG_DIR}/signature`
/// 5. `${HOME}/.signature`
/// 6. No signature otherwise.
///
/// Default: `false`
#[serde(default = "false_val", alias = "use-signature")]
pub use_signature: bool,
/// Signature delimiter, that is, text that will be prefixed to your
/// signature to separate it from the email body.
///
/// Default: `"\n\n-- \n"`
#[serde(default, alias = "signature-delimiter")]
pub signature_delimiter: Option<String>,
}
impl Default for ComposingSettings {
fn default() -> Self {
ComposingSettings {
send_mail: SendMail::ShellCommand("false".into()),
Self {
editor_command: None,
embedded_pty: false,
format_flowed: true,
insert_user_agent: true,
default_header_values: HashMap::default(),
default_header_values: IndexMap::default(),
store_sent_mail: true,
wrap_header_preamble: None,
attribution_format_string: None,
attribution_use_posix_locale: true,
forward_as_attachment: ToggleFlag::Ask,
forward_as_attachment: ActionFlag::Ask,
reply_prefix_list_to_strip: None,
reply_prefix: res(),
custom_compose_hooks: vec![],
disabled_compose_hooks: vec![],
signature_file: None,
use_signature: false,
signature_delimiter: None,
}
}
}
@ -176,7 +208,7 @@ pub mod strings {
named_unit_variant!(server_submission);
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
pub enum SendMail {
#[cfg(feature = "smtp")]
@ -186,9 +218,17 @@ pub enum SendMail {
ShellCommand(String),
}
impl Default for SendMail {
/// Returns the `false` POSIX shell utility, in order to return an error
/// when called.
fn default() -> Self {
Self::ShellCommand("false".into())
}
}
/// Shell command compose hooks (See
/// [`crate::mail::compose::hooks::Hook`])
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ComposeHook {
#[serde(deserialize_with = "non_empty_string")]
@ -202,3 +242,112 @@ impl From<ComposeHook> for crate::mail::hooks::Hook {
Self::new_shell_command(c.name.into(), c.command)
}
}
const SENDMAIL_ERR_HELP: &str = r#"Invalid `send_mail` value.
Here are some valid examples:
Use server submission in protocols that support it (JMAP, NNTP)
===============================================================
send_mail = "server_submission"
Using a shell script
====================
send_mail = "msmtp --read-recipients --read-envelope-from"
Direct SMTP connection
======================
[accounts.account-name]
send_mail = { hostname = "mail.example.com", port = 587, auth = { type = "auto", password = { type = "raw", value = "hunter2" } }, security = { type = "STARTTLS" } }
[accounts.account-name.send_mail]
hostname = "mail.example.com"
port = 587
auth = { type = "auto", password = { type = "command_eval", value = "/path/to/password_script.sh" } }
security = { type = "TLS", danger_accept_invalid_certs = true } }
`send_mail` direct SMTP connection fields:
- hostname: text
- port: valid port number
- envelope_from: text (optional, default is empty),
- auth: ...
- security: ... (optional, default is "auto")
- extensions: ... (optional, default is PIPELINING, CHUNKING, PRDR, 8BITMIME, BINARYMIME, SMTPUTF8, AUTH and DSN_NOTIFY)
Possible values for `send_mail.auth`:
No authentication:
auth = { type = "none" }
Regular authentication:
Note: `require_auth` and `auth_type` are optional and can be skipped.
auth = { type = "auto", username = "...", password = "...", require_auth = true, auth_type = ... }
password can be:
password = { type = "raw", value = "..." }
password = { type = "command_eval", value = "/path/to/password_script.sh" }
XOAuth2 authentication:
Note: `require_auth` is optional and can be skipped.
auth = { type = "xoauth2", token_command = "...", require_auth = true }
Possible values for `send_mail.auth.auth_type` when `auth.type` is "auto":
auth_type = { plain = false, login = true }
Possible values for `send_mail.security`:
Note that in all cases field `danger_accept_invalid_certs` is optional and its default value is false.
security = "none"
security = { type = "auto", danger_accept_invalid_certs = false }
security = { type = "STARTTLS", danger_accept_invalid_certs = false }
security = { type = "TLS", danger_accept_invalid_certs = false }
Possible values for `send_mail.extensions` (All optional and have default values `true`:
pipelining
chunking
8bitmime
prdr
binarymime
smtputf8
auth
dsn_notify: Array of options e.g. ["FAILURE"]
"#;
impl<'de> Deserialize<'de> for SendMail {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
#[derive(Deserialize)]
#[serde(untagged)]
enum SendMailInner {
#[cfg(feature = "smtp")]
Smtp(melib::smtp::SmtpServerConf),
#[serde(with = "strings::server_submission")]
ServerSubmission,
ShellCommand(String),
}
match melib::serde_path_to_error::deserialize(deserializer) {
#[cfg(feature = "smtp")]
Ok(SendMailInner::Smtp(v)) => Ok(Self::Smtp(v)),
Ok(SendMailInner::ServerSubmission) => Ok(Self::ServerSubmission),
Ok(SendMailInner::ShellCommand(v)) => Ok(Self::ShellCommand(v)),
Err(err)
if err.inner().to_string() == D::Error::missing_field("send_mail").to_string() =>
{
// Surely there should be a better way to do this...
Err(err.into_inner())
}
Err(_err) => Err(de::Error::custom(SENDMAIL_ERR_HELP)),
}
}
}

View file

@ -0,0 +1,294 @@
//
// meli
//
// Copyright 2017- Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use crate::conf::*;
pub trait DotAddressable: serde::Serialize {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
if !path.is_empty() {
Err(Error::new(format!(
"{} has no fields, it is of type {}",
parent_field,
std::any::type_name::<Self>()
)))
} else {
Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string())
}
}
}
impl DotAddressable for bool {}
impl DotAddressable for String {}
impl DotAddressable for char {}
impl DotAddressable for data_types::IndexStyle {}
impl DotAddressable for data_types::SearchBackend {}
impl DotAddressable for data_types::ThreadLayout {}
impl DotAddressable for u64 {}
impl DotAddressable for TagHash {}
impl DotAddressable for crate::terminal::Color {}
impl DotAddressable for crate::terminal::Attr {}
impl DotAddressable for crate::terminal::Key {}
impl DotAddressable for usize {}
impl DotAddressable for Query {}
impl DotAddressable for melib::LogLevel {}
impl DotAddressable for PathBuf {}
impl DotAddressable for ToggleFlag {}
impl DotAddressable for ActionFlag {}
impl DotAddressable for melib::SpecialUsageMailbox {}
impl<T: DotAddressable> DotAddressable for Option<T> {}
impl<T: DotAddressable> DotAddressable for Vec<T> {}
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable>
// DotAddressable for HashMap<K, V>
// {
// }
// impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for
// HashSet<K> {}
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash, V: DotAddressable> DotAddressable
for indexmap::IndexMap<K, V>
{
}
impl<K: DotAddressable + std::cmp::Eq + std::hash::Hash> DotAddressable for indexmap::IndexSet<K> {}
impl DotAddressable for (SortField, SortOrder) {}
impl DotAddressable for LogSettings {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"log_file" => self.log_file.lookup(field, tail),
"maximum_level" => self.maximum_level.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for Settings {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"accounts" => self.accounts.lookup(field, tail),
"pager" => self.pager.lookup(field, tail),
"listing" => self.listing.lookup(field, tail),
"notifications" => self.notifications.lookup(field, tail),
"shortcuts" => self.shortcuts.lookup(field, tail),
"tags" => Err(Error::new("unimplemented")),
"composing" => Err(Error::new("unimplemented")),
"pgp" => Err(Error::new("unimplemented")),
"terminal" => self.terminal.lookup(field, tail),
"log" => self.log.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for AccountConf {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"account" => self.account.lookup(field, tail),
"conf" => self.conf.lookup(field, tail),
"conf_override" => self.conf_override.lookup(field, tail),
"mailbox_confs" => self.mailbox_confs.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for MailUIConf {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let _tail = &path[1..];
match *field {
"pager" => Err(Error::new("unimplemented")), /* self.pager.lookup(field, */
// tail),
"listing" => Err(Error::new("unimplemented")), /* self.listing.lookup(field, */
// tail),
"notifications" => Err(Error::new("unimplemented")), /* self.notifications.lookup(field, tail), */
"shortcuts" => Err(Error::new("unimplemented")), /* self.shortcuts. */
// lookup(field,
// tail),
"composing" => Err(Error::new("unimplemented")), /* self.composing. */
// lookup(field, tail),
"identity" => Err(Error::new("unimplemented")), /* self.identity. */
// lookup(field,
// tail)<String>,
"tags" => Err(Error::new("unimplemented")), /* self.tags.lookup(field, */
// tail),
"themes" => Err(Error::new("unimplemented")), /* self.themes. */
// lookup(field,
// tail)<Themes>,
"pgp" => Err(Error::new("unimplemented")), //self.pgp.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for FileMailboxConf {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"conf_override" => self.conf_override.lookup(field, tail),
"mailbox_conf" => self.mailbox_conf.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for FileAccount {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"root_mailbox" => self.root_mailbox.lookup(field, tail),
"format" => self.format.lookup(field, tail),
"identity" => self.identity.lookup(field, tail),
"display_name" => self.display_name.lookup(field, tail),
"read_only" => self.read_only.lookup(field, tail),
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
"mailboxes" => self.mailboxes.lookup(field, tail),
"search_backend" => self.search_backend.lookup(field, tail),
"manual_refresh" => self.manual_refresh.lookup(field, tail),
"refresh_command" => self.refresh_command.lookup(field, tail),
"conf_override" => self.conf_override.lookup(field, tail),
"extra" => self.extra.lookup(field, tail),
"order" => self.order.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for melib::AccountSettings {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"name" => self.name.lookup(field, tail),
"root_mailbox" => self.root_mailbox.lookup(field, tail),
"format" => self.format.lookup(field, tail),
"identity" => self.identity.lookup(field, tail),
"read_only" => self.read_only.lookup(field, tail),
"display_name" => self.display_name.lookup(field, tail),
"subscribed_mailboxes" => self.subscribed_mailboxes.lookup(field, tail),
"mailboxes" => self.mailboxes.lookup(field, tail),
"manual_refresh" => self.manual_refresh.lookup(field, tail),
"extra" => self.extra.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
impl DotAddressable for melib::MailboxConf {
fn lookup(&self, parent_field: &str, path: &[&str]) -> Result<String> {
match path.first() {
Some(field) => {
let tail = &path[1..];
match *field {
"alias" => self.alias.lookup(field, tail),
"autoload" => self.autoload.lookup(field, tail),
"subscribe" => self.subscribe.lookup(field, tail),
"ignore" => self.ignore.lookup(field, tail),
"usage" => self.usage.lookup(field, tail),
"extra" => self.extra.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}

View file

@ -0,0 +1,163 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
pub mod dotaddressable;
pub mod regex_pattern;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum IndexStyle {
Plain,
Threaded,
#[default]
Compact,
Conversations,
}
impl<'de> Deserialize<'de> for IndexStyle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
match s.as_str() {
plain if plain.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
threaded if threaded.eq_ignore_ascii_case("threaded") => Ok(Self::Threaded),
compact if compact.eq_ignore_ascii_case("compact") => Ok(Self::Compact),
conversations if conversations.eq_ignore_ascii_case("conversations") => {
Ok(Self::Conversations)
}
_ => Err(de::Error::custom(
"invalid `index_style` value, expected one of: \"plain\", \"threaded\", \
\"compact\" or \"conversations\".",
)),
}
}
}
impl Serialize for IndexStyle {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Plain => serializer.serialize_str("plain"),
Self::Threaded => serializer.serialize_str("threaded"),
Self::Compact => serializer.serialize_str("compact"),
Self::Conversations => serializer.serialize_str("conversations"),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum SearchBackend {
None,
#[default]
Auto,
#[cfg(feature = "sqlite3")]
Sqlite3,
}
impl<'de> Deserialize<'de> for SearchBackend {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
match s.as_str() {
#[cfg(feature = "sqlite3")]
sqlite3
if sqlite3.eq_ignore_ascii_case("sqlite3")
|| sqlite3.eq_ignore_ascii_case("sqlite") =>
{
Ok(Self::Sqlite3)
}
none if none.eq_ignore_ascii_case("none")
|| none.eq_ignore_ascii_case("nothing")
|| none.is_empty() =>
{
Ok(Self::None)
}
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
_ => Err(de::Error::custom(if cfg!(feature = "sqlite3") {
"invalid `search_backend` value, expected one of: \"sqlite3\", \"sqlite\", \
\"none\" or \"auto\"."
} else {
"invalid `search_backend` value, expected one of: \"none\" or \"auto\"."
})),
}
}
}
impl Serialize for SearchBackend {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
#[cfg(feature = "sqlite3")]
Self::Sqlite3 => serializer.serialize_str("sqlite3"),
Self::None => serializer.serialize_str("none"),
Self::Auto => serializer.serialize_str("auto"),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
pub enum ThreadLayout {
Vertical,
Horizontal,
#[default]
Auto,
}
impl<'de> Deserialize<'de> for ThreadLayout {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <String>::deserialize(deserializer)?;
match s.as_str() {
vertical if vertical.eq_ignore_ascii_case("vertical") => Ok(Self::Vertical),
horizontal if horizontal.eq_ignore_ascii_case("horizontal") => Ok(Self::Horizontal),
auto if auto.eq_ignore_ascii_case("auto") => Ok(Self::Auto),
_ => Err(de::Error::custom(
"invalid `thread_layout` value, expected one of: \"vertical\", \"horizontal\" or \
\"auto\".",
)),
}
}
}
impl Serialize for ThreadLayout {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Vertical => serializer.serialize_str("vertical"),
Self::Horizontal => serializer.serialize_str("horizontal"),
Self::Auto => serializer.serialize_str("auto"),
}
}
}

View file

@ -0,0 +1,215 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use melib::error::{Result, WrapResultIntoError};
use serde::{Deserialize, Deserializer};
const fn lf_val() -> u8 {
b'\n'
}
#[derive(Clone, Debug)]
pub enum RegexValue {
Default {
pattern: regex::Regex,
},
Builder {
pattern: regex::Regex,
options: RegexOptions,
},
}
impl RegexValue {
pub fn new_with_options(pattern: &str, o: RegexOptions) -> Result<Self> {
let mut b = regex::RegexBuilder::new(pattern);
b.unicode(o.unicode)
.case_insensitive(o.case_insensitive)
.multi_line(o.multi_line)
.dot_matches_new_line(o.dot_matches_new_line)
.crlf(o.crlf)
.line_terminator(o.line_terminator)
.swap_greed(o.swap_greed)
.ignore_whitespace(o.ignore_whitespace)
.octal(o.octal);
if let Some(v) = o.size_limit {
b.size_limit(v);
}
let pattern = b
.build()
.wrap_err(|| format!("Could not compile regular expression `{}`", pattern))?;
Ok(Self::Builder {
pattern,
options: o,
})
}
pub fn find_iter<'w, 's>(&'w self, s: &'s str) -> FindIter<'w, 's> {
let (Self::Default { pattern } | Self::Builder { pattern, .. }) = self;
FindIter {
iter: pattern.find_iter(s),
char_indices: s.char_indices(),
char_offset: 0,
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct RegexOptions {
#[serde(default = "crate::conf::true_val")]
unicode: bool,
#[serde(default = "crate::conf::false_val")]
case_insensitive: bool,
#[serde(default = "crate::conf::false_val")]
multi_line: bool,
#[serde(default = "crate::conf::false_val")]
dot_matches_new_line: bool,
#[serde(default = "crate::conf::false_val")]
crlf: bool,
#[serde(default = "lf_val")]
line_terminator: u8,
#[serde(default = "crate::conf::false_val")]
swap_greed: bool,
#[serde(default = "crate::conf::false_val")]
ignore_whitespace: bool,
#[serde(default = "crate::conf::false_val")]
octal: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
size_limit: Option<usize>,
}
impl Default for RegexOptions {
fn default() -> Self {
Self {
unicode: true,
case_insensitive: false,
multi_line: false,
dot_matches_new_line: false,
crlf: false,
line_terminator: b'\n',
swap_greed: false,
ignore_whitespace: false,
octal: false,
size_limit: None,
}
}
}
impl<'de> Deserialize<'de> for RegexValue {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// [ref:FIXME]: clippy false positive, remove when resolved.
#![allow(clippy::collection_is_never_read)]
#[derive(Deserialize)]
#[serde(untagged)]
enum Inner<'a> {
Default {
pattern: &'a str,
},
Builder {
pattern: &'a str,
#[serde(flatten)]
o: RegexOptions,
},
}
let s = <Inner>::deserialize(deserializer);
Ok(
match s.map_err(|err| {
serde::de::Error::custom(format!(
r#"expected one of "true", "false", "ask", found `{}`"#,
err
))
})? {
Inner::Default { pattern } => Self::Default {
pattern: regex::Regex::new(pattern).map_err(|err| {
serde::de::Error::custom(format!(
"Could not compile regular expression `{}`: {}",
pattern, err
))
})?,
},
Inner::Builder { pattern, o } => {
let mut b = regex::RegexBuilder::new(pattern);
b.unicode(o.unicode)
.case_insensitive(o.case_insensitive)
.multi_line(o.multi_line)
.dot_matches_new_line(o.dot_matches_new_line)
.crlf(o.crlf)
.line_terminator(o.line_terminator)
.swap_greed(o.swap_greed)
.ignore_whitespace(o.ignore_whitespace)
.octal(o.octal);
if let Some(v) = o.size_limit {
b.size_limit(v);
}
let pattern = b.build().map_err(|err| {
serde::de::Error::custom(format!(
"Could not compile regular expression `{}`: {}",
pattern, err
))
})?;
Self::Builder {
pattern,
options: o,
}
}
},
)
}
}
pub struct FindIter<'r, 's> {
iter: regex::Matches<'r, 's>,
char_indices: std::str::CharIndices<'s>,
char_offset: usize,
}
impl Iterator for FindIter<'_, '_> {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
let next_byte_offset = self.iter.next()?;
let mut next_char_index = self.char_indices.next()?;
while next_byte_offset.start() < next_char_index.0 {
self.char_offset += 1;
next_char_index = self.char_indices.next()?;
}
let start = self.char_offset;
while next_byte_offset.end()
> self
.char_indices
.next()
.map(|(v, _)| v)
.unwrap_or_else(|| next_byte_offset.end())
{
self.char_offset += 1;
}
let end = self.char_offset + 1;
Some((start, end))
}
}

View file

@ -0,0 +1,65 @@
//
// meli
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! default value functions for deserializing
pub fn false_val<T: From<bool>>() -> T {
false.into()
}
pub fn true_val<T: From<bool>>() -> T {
true.into()
}
pub fn zero_val<T: From<usize>>() -> T {
0.into()
}
pub fn eighty_val<T: From<usize>>() -> T {
80.into()
}
pub fn none<T>() -> Option<T> {
None
}
pub fn internal_value_false<T: From<melib::conf::ToggleFlag>>() -> T {
melib::conf::ToggleFlag::InternalVal(false).into()
}
pub fn internal_value_true<T: From<melib::conf::ToggleFlag>>() -> T {
melib::conf::ToggleFlag::InternalVal(true).into()
}
pub fn action_internal_value_false<T: From<melib::ActionFlag>>() -> T {
melib::conf::ActionFlag::InternalVal(false).into()
}
//pub fn action_internal_value_true<
// T: From<melib::conf::ActionFlag>,
//>() -> T {
// melib::conf::ActionFlag::InternalVal(true).into()
//}
pub fn ask<T: From<melib::conf::ActionFlag>>() -> T {
melib::conf::ActionFlag::Ask.into()
}

View file

@ -19,9 +19,13 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::{search::Query, Error, Result};
use melib::{search::Query, Error, Result, ToggleFlag};
use super::{default_vals::*, DotAddressable, IndexStyle};
use crate::conf::{
data_types::{IndexStyle, ThreadLayout},
default_values::*,
DotAddressable,
};
/// Settings for mail listings
///
@ -55,7 +59,7 @@ use super::{default_vals::*, DotAddressable, IndexStyle};
/// const HAS_SIBLING_LEAF: &str = " ├─";
/// const NO_SIBLING_LEAF: &str = " ╰─";
/// ```
#[derive(Debug, Deserialize, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct ListingSettings {
/// Number of context lines when going to next page.
@ -130,6 +134,17 @@ pub struct ListingSettings {
#[serde(default)]
pub attachment_flag: Option<String>,
/// Flag to show if any thread entry contains your address as a receiver.
/// Useful to make mailing list threads that CC you stand out.
/// Default: "✸"
#[serde(default)]
pub highlight_self_flag: Option<String>,
/// Show `highlight_self_flag` or not.
/// Default: false
#[serde(default)]
pub highlight_self: ToggleFlag,
/// Should threads with different Subjects show a list of those
/// subjects on the entry title?
/// Default: "true"
@ -156,6 +171,14 @@ pub struct ListingSettings {
/// Hide sidebar on launch. Default: "false"
#[serde(default = "false_val", alias = "hide-sidebar-on-launch")]
pub hide_sidebar_on_launch: bool,
/// Default: ' '
#[serde(default = "default_divider")]
pub mail_view_divider: char,
/// Default: "auto"
#[serde(default)]
pub thread_layout: ThreadLayout,
}
const fn default_divider() -> char {
@ -185,11 +208,15 @@ impl Default for ListingSettings {
thread_snoozed_flag: None,
selected_flag: None,
attachment_flag: None,
highlight_self_flag: None,
highlight_self: ToggleFlag::Unset,
thread_subject_pack: true,
threaded_repeat_identical_from_values: false,
relative_menu_indices: true,
relative_list_indices: true,
hide_sidebar_on_launch: false,
mail_view_divider: default_divider(),
thread_layout: ThreadLayout::default(),
}
}
}
@ -224,6 +251,8 @@ impl DotAddressable for ListingSettings {
"thread_snoozed_flag" => self.thread_snoozed_flag.lookup(field, tail),
"selected_flag" => self.selected_flag.lookup(field, tail),
"attachment_flag" => self.attachment_flag.lookup(field, tail),
"highlight_self_flag" => self.highlight_self_flag.lookup(field, tail),
"highlight_self" => self.highlight_self.lookup(field, tail),
"thread_subject_pack" => self.thread_subject_pack.lookup(field, tail),
"threaded_repeat_identical_from_values" => self
.threaded_repeat_identical_from_values
@ -231,13 +260,17 @@ impl DotAddressable for ListingSettings {
"relative_menu_indices" => self.relative_menu_indices.lookup(field, tail),
"relative_list_indices" => self.relative_list_indices.lookup(field, tail),
"hide_sidebar_on_launch" => self.hide_sidebar_on_launch.lookup(field, tail),
"mail_view_divider" => self.mail_view_divider.lookup(field, tail),
"thread_layout" => self.thread_layout.lookup(field, tail),
other => Err(Error::new(format!(
"{} has no field named {}",
parent_field, other
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}

View file

@ -21,13 +21,13 @@
use melib::{Error, Result, ToggleFlag};
use super::{
default_vals::{internal_value_false, none, true_val},
use crate::conf::{
default_values::{internal_value_false, none, true_val},
DotAddressable,
};
/// Settings for the notifications function.
#[derive(Debug, Serialize, Deserialize, Clone)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct NotificationsSettings {
/// Enable notifications.
@ -88,7 +88,9 @@ impl DotAddressable for NotificationsSettings {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -21,12 +21,13 @@
//! Settings for the pager function.
use indexmap::IndexMap;
use melib::{Error, Result, ToggleFlag};
use super::{default_vals::*, deserializers::*, DotAddressable};
use crate::conf::{default_values::*, deserializers::*, DotAddressable};
/// Settings for the pager function.
#[derive(Debug, Deserialize, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PagerSettings {
/// Number of context lines when going to next page.
@ -60,6 +61,12 @@ pub struct PagerSettings {
#[serde(default = "none", deserialize_with = "non_empty_opt_string")]
pub filter: Option<String>,
/// Named filter commands to use at will.
///
/// Default: empty
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub named_filters: IndexMap<String, String>,
/// A command to pipe html output before displaying it in a pager
/// Default: None
#[serde(
@ -126,6 +133,7 @@ impl Default for PagerSettings {
sticky_headers: false,
pager_ratio: 80,
filter: None,
named_filters: IndexMap::default(),
html_filter: None,
html_open: None,
format_flowed: true,
@ -150,6 +158,7 @@ impl DotAddressable for PagerSettings {
"sticky_headers" => self.sticky_headers.lookup(field, tail),
"pager_ratio" => self.pager_ratio.lookup(field, tail),
"filter" => self.filter.lookup(field, tail),
"named_filters" => self.named_filters.lookup(field, tail),
"html_filter" => self.html_filter.lookup(field, tail),
"html_open" => self.html_open.lookup(field, tail),
"format_flowed" => self.format_flowed.lookup(field, tail),
@ -167,7 +176,9 @@ impl DotAddressable for PagerSettings {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}

View file

@ -19,33 +19,33 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::conf::ToggleFlag;
use melib::conf::ActionFlag;
use super::default_vals::*;
use crate::conf::default_values::*;
/// Settings for digital signing and encryption
#[derive(Debug, Deserialize, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct PGPSettings {
/// auto verify signed e-mail according to RFC3156
/// Default: true
#[serde(default = "true_val", alias = "auto-verify-signatures")]
pub auto_verify_signatures: bool,
pub auto_verify_signatures: ActionFlag,
/// auto decrypt encrypted e-mail
/// Default: true
#[serde(default = "true_val", alias = "auto-decrypt")]
pub auto_decrypt: bool,
pub auto_decrypt: ActionFlag,
/// always sign sent e-mail
/// Default: false
#[serde(default = "false_val", alias = "auto-sign")]
pub auto_sign: bool,
pub auto_sign: ActionFlag,
/// Auto encrypt sent e-mail
/// Default: false
#[serde(default = "false_val", alias = "auto-encrypt")]
pub auto_encrypt: bool,
pub auto_encrypt: ActionFlag,
// https://tools.ietf.org/html/rfc4880#section-12.2
/// Default: None
@ -60,10 +60,17 @@ pub struct PGPSettings {
#[serde(default = "none", alias = "encrypt-key")]
pub encrypt_key: Option<String>,
/// Default: true
#[serde(default = "true_val", alias = "encrypt-for-self")]
pub encrypt_for_self: bool,
/// Allow remote lookups
/// Default: None
#[serde(default = "internal_value_false", alias = "allow-remote-lookups")]
pub allow_remote_lookup: ToggleFlag,
/// Default: False
#[serde(
default = "action_internal_value_false",
alias = "allow-remote-lookups"
)]
pub allow_remote_lookup: ActionFlag,
/// Remote lookup mechanisms.
/// Default: "local,wkd"
@ -91,15 +98,16 @@ fn default_lookup_mechanism() -> melib::gpgme::LocateKey {
impl Default for PGPSettings {
fn default() -> Self {
PGPSettings {
auto_verify_signatures: true,
auto_decrypt: true,
auto_sign: false,
auto_encrypt: false,
Self {
auto_verify_signatures: true.into(),
auto_decrypt: true.into(),
auto_sign: false.into(),
auto_encrypt: false.into(),
encrypt_for_self: true,
sign_key: None,
decrypt_key: None,
encrypt_key: None,
allow_remote_lookup: internal_value_false::<ToggleFlag>(),
allow_remote_lookup: action_internal_value_false::<ActionFlag>(),
#[cfg(feature = "gpgme")]
remote_lookup_mechanisms: default_lookup_mechanism(),
#[cfg(not(feature = "gpgme"))]

View file

@ -0,0 +1,281 @@
//
// meli
//
// Copyright 2017 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
//! Preprocess configuration files by unfolding `include` macros.
use std::{
io::{self, BufRead, Read, Write},
path::{Path, PathBuf},
process::{Command, Stdio},
sync::Arc,
};
use melib::{
error::{Error, ErrorKind, Result, ResultIntoError, WrapResultIntoError},
utils::parsec::*,
ShellExpandTrait,
};
/// Try to parse line into a path to be included.
pub fn include_directive<'a>() -> impl Parser<'a, Option<&'a str>> {
move |input: &'a str| {
enum State {
Start,
Path,
}
use State::*;
let mut state = State::Start;
let mut i = 0;
while i < input.len() {
match (&state, input.as_bytes()[i]) {
(Start, b'#') => {
return Ok(("", None));
}
(Start, b) if (b as char).is_whitespace() => { /* consume */ }
(Start, _) if input.as_bytes()[i..].starts_with(b"include(") => {
i += "include(".len();
state = Path;
continue;
}
(Start, _) => {
return Ok(("", None));
}
(Path, b'"') | (Path, b'\'') | (Path, b'`') => {
let mut end = i + 1;
while end < input.len() && input.as_bytes()[end] != input.as_bytes()[i] {
end += 1;
}
if end == input.len() {
return Err(input);
}
let ret = &input[i + 1..end];
end += 1;
if end < input.len() && input.as_bytes()[end] != b')' {
/* Nothing else allowed in line */
return Err(input);
}
end += 1;
while end < input.len() {
if !(input.as_bytes()[end] as char).is_whitespace() {
/* Nothing else allowed in line */
return Err(input);
}
end += 1;
}
return Ok(("", Some(ret)));
}
(Path, _) => return Err(input),
}
i += 1;
}
Ok(("", None))
}
}
/// Expands `include` macros in path.
fn pp_helper(path: &Path, level: u8) -> Result<String> {
if level > 7 {
return Err(Error::new(format!(
"Maximum recursion limit reached while unfolding include directives in {}. Have you \
included a config file within itself?",
path.display()
))
.set_kind(ErrorKind::ValueError));
}
let mut contents = String::new();
let mut file = std::fs::File::open(path)?;
file.read_to_string(&mut contents)?;
let mut ret = String::with_capacity(contents.len());
for (i, l) in contents.lines().enumerate() {
if let (_, Some(sub_path)) = include_directive().parse(l).map_err(|l| {
Error::new(format!(
"Malformed include directive in line {} of file {}: {}\nConfiguration uses the \
standard m4 macro include(\"filename\").",
i,
path.display(),
l
))
.set_kind(ErrorKind::ValueError)
})? {
let mut p = Path::new(sub_path).expand();
if p.is_relative() {
/* We checked that path is ok above so we can do unwrap here */
let prefix = path.parent().unwrap();
p = prefix.join(p)
}
ret.push_str(&pp_helper(&p, level + 1).chain_err_related_path(&p)?);
} else {
ret.push_str(l);
ret.push('\n');
}
}
Ok(ret)
}
fn pp_inner(path: &Path) -> Result<String> {
let p_buf: PathBuf = if path.is_relative() {
path.expand().canonicalize()?
} else {
path.expand()
};
let mut ret = expand_config(&p_buf)?;
if let Ok(xdg_dirs) = xdg::BaseDirectories::with_prefix("meli") {
for theme_mailbox in xdg_dirs.find_config_files("themes") {
let read_dir =
std::fs::read_dir(&theme_mailbox).chain_err_related_path(&theme_mailbox)?;
for theme in read_dir {
let theme_path = theme?.path();
if let Some(extension) = theme_path.extension() {
if extension == "toml" {
ret.push_str(
&pp_helper(&theme_path, 0).chain_err_related_path(&theme_path)?,
);
}
}
}
}
}
Ok(ret)
}
/// Expands `include` macros in configuration file and other configuration
/// files (eg. themes) in the filesystem.
pub fn pp(path: &Path) -> Result<String> {
pp_inner(path)
.wrap_err(|| "Could not preprocess configuration file")
.chain_err_related_path(path)
.chain_err_kind(ErrorKind::Configuration)
}
pub fn expand_config(conf_path: &Path) -> Result<String> {
fn inner(conf_path: &Path) -> Result<String> {
let _paths = get_included_configs(conf_path)?;
const M4_PREAMBLE: &str = r#"define(`builtin_include', defn(`include'))dnl
define(`include', `builtin_include(substr($1,1,decr(decr(len($1)))))dnl')dnl
"#;
let mut contents = String::new();
contents.clear();
let mut file = std::fs::File::open(conf_path)?;
file.read_to_string(&mut contents)?;
let mut handle = Command::new("m4")
.current_dir(conf_path.parent().unwrap_or_else(|| Path::new("/")))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
let mut stdin = handle.stdin.take().unwrap();
stdin.write_all(M4_PREAMBLE.as_bytes())?;
stdin.write_all(contents.as_bytes())?;
drop(stdin);
let stdout = handle.wait_with_output()?.stdout;
Ok(String::from_utf8_lossy(&stdout).to_string())
}
inner(conf_path).chain_err_related_path(conf_path)
}
pub fn get_included_configs(conf_path: &Path) -> Result<Vec<PathBuf>> {
const M4_PREAMBLE: &str = r#"divert(-1)dnl
define(`include', `divert(0)$1
divert(-1)
')dnl
changequote(`"', `"')dnl
"#;
let mut ret = vec![];
let prefix = conf_path.parent().unwrap().to_path_buf();
let mut stack = vec![(None::<PathBuf>, conf_path.to_path_buf())];
let mut contents = String::new();
while let Some((parent, p)) = stack.pop() {
if !p.exists() || p.is_dir() {
return Err(Error::new(format!(
"Path {}{included}{in_parent} {msg}.",
p.display(),
included = if parent.is_some() {
" which is included in "
} else {
""
},
in_parent = if let Some(parent) = parent {
std::borrow::Cow::Owned(parent.display().to_string())
} else {
std::borrow::Cow::Borrowed("")
},
msg = if !p.exists() {
"does not exist"
} else {
"is a directory, not a text file"
}
))
.set_kind(ErrorKind::ValueError));
}
contents.clear();
let mut file = std::fs::File::open(&p).chain_err_related_path(&p)?;
file.read_to_string(&mut contents)
.chain_err_related_path(&p)?;
let mut handle = match Command::new("m4")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(handle) => handle,
Err(err) => match err.kind() {
io::ErrorKind::NotFound => {
return Err(Error::new(
"`m4` executable not found in PATH. Please provide an m4 binary.",
)
.set_kind(ErrorKind::Platform))
}
_ => {
return Err(Error::new("Could not process configuration with `m4`")
.set_source(Some(Arc::new(err)))
.set_kind(ErrorKind::Platform))
}
},
};
let mut stdin = handle.stdin.take().unwrap();
stdin.write_all(M4_PREAMBLE.as_bytes())?;
stdin.write_all(contents.as_bytes())?;
drop(stdin);
let stdout = handle.wait_with_output()?.stdout.clone();
for subpath in stdout.lines() {
let subpath = subpath?;
let path = &Path::new(&subpath);
if path.is_absolute() {
stack.push((Some(p.to_path_buf()), path.to_path_buf()));
} else {
stack.push((Some(p.to_path_buf()), prefix.join(path)));
}
}
ret.push(p.to_path_buf());
}
Ok(ret)
}

View file

@ -35,7 +35,7 @@ macro_rules! shortcut {
};
}
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct Shortcuts {
#[serde(default)]
@ -83,11 +83,19 @@ impl DotAddressable for Shortcuts {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CommandShortcut {
pub shortcut: Key,
pub command: Vec<String>,
}
/// Create a struct holding all of a Component's shortcuts.
#[macro_export]
macro_rules! shortcut_key_values {
@ -100,6 +108,7 @@ macro_rules! shortcut_key_values {
#[serde(default)]
#[serde(rename = $cname)]
pub struct $name {
pub commands: Vec<CommandShortcut>,
$(pub $fname : Key),*
}
@ -122,6 +131,7 @@ macro_rules! shortcut_key_values {
impl Default for $name {
fn default() -> Self {
Self {
commands : vec![],
$($fname: $default),*
}
}
@ -140,7 +150,7 @@ macro_rules! shortcut_key_values {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self).map_err(|err| err.to_string())?.to_string()),
}
}
}
@ -164,10 +174,12 @@ shortcut_key_values! { "listing",
search |> "Search within list of e-mails." |> Key::Char('/'),
refresh |> "Manually request a mailbox refresh." |> Key::F(5),
set_seen |> "Set thread as seen." |> Key::Char('n'),
send_to_trash |> "Send entry to trash folder." |> Key::Char('D'),
union_modifier |> "Union modifier." |> Key::Ctrl('u'),
diff_modifier |> "Difference modifier." |> Key::Ctrl('d'),
intersection_modifier |> "Intersection modifier." |> Key::Ctrl('i'),
select_entry |> "Select thread entry." |> Key::Char('v'),
select_entry |> "Select thread entry." |> Key::Char('V'),
select_motion |> "Perform select motion with a movement." |> Key::Char('v'),
increase_sidebar |> "Increase sidebar width." |> Key::Ctrl('f'),
decrease_sidebar |> "Decrease sidebar width." |> Key::Ctrl('d'),
next_entry |> "Focus on next entry." |> Key::Ctrl('n'),
@ -187,6 +199,7 @@ shortcut_key_values! { "contact-list",
scroll_down |> "Scroll down list." |> Key::Char('j'),
create_contact |> "Create new contact." |> Key::Char('c'),
edit_contact |> "Edit contact under cursor." |> Key::Char('e'),
export_contact |> "Export contact under cursor to .vcf." |> Key::Char('E'),
delete_contact |> "Delete contact under cursor." |> Key::Char('d'),
mail_contact |> "Mail contact under cursor." |> Key::Char('m'),
next_account |> "Go to next account." |> Key::Char('H'),
@ -201,7 +214,8 @@ shortcut_key_values! { "pager",
page_down |> "Go to next pager page." |> Key::PageDown,
page_up |> "Go to previous pager page." |> Key::PageUp,
scroll_down |> "Scroll down pager." |> Key::Char('j'),
scroll_up |> "Scroll up pager." |> Key::Char('k')
scroll_up |> "Scroll up pager." |> Key::Char('k'),
select_filter |> "Select content filter." |> Key::Char('f')
}
}
@ -221,8 +235,8 @@ shortcut_key_values! { "general",
home_page |> "Go to first page. (catch-all setting)" |> Key::Home,
end_page |> "Go to last page. (catch-all setting)" |> Key::End,
open_entry |> "Open list entry. (catch-all setting)" |> Key::Char('\n'),
info_message_next |> "Show next info message, if any." |> Key::Alt('>'),
info_message_previous |> "Show previous info message, if any." |> Key::Alt('<'),
info_message_next |> "Show next info message, if any." |> Key::Char('>'),
info_message_previous |> "Show previous info message, if any." |> Key::Char('<'),
focus_in_text_field |> "Focus on a text field." |> Key::Char('\n'),
next_search_result |> "Scroll to next search result." |> Key::Char('n'),
previous_search_result |> "Scroll to previous search result." |> Key::Char('N')

View file

@ -21,24 +21,22 @@
//! E-mail tag configuration and {de,}serializing.
use std::collections::{HashMap, HashSet};
use indexmap::{IndexMap, IndexSet};
use melib::{Error, Result, TagHash};
use serde::{Deserialize, Deserializer};
use super::DotAddressable;
use crate::terminal::Color;
use crate::{conf::DotAddressable, terminal::Color};
#[derive(Default, Debug, Deserialize, Clone, Serialize)]
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TagsSettings {
#[serde(default, deserialize_with = "tag_color_de")]
pub colors: HashMap<TagHash, Color>,
pub colors: IndexMap<TagHash, Color>,
#[serde(default, deserialize_with = "tag_set_de", alias = "ignore-tags")]
pub ignore_tags: HashSet<TagHash>,
pub ignore_tags: IndexSet<TagHash>,
}
pub fn tag_set_de<'de, D, T: std::convert::From<HashSet<TagHash>>>(
pub fn tag_set_de<'de, D, T: std::convert::From<IndexSet<TagHash>>>(
deserializer: D,
) -> std::result::Result<T, D::Error>
where
@ -47,11 +45,11 @@ where
Ok(<Vec<String>>::deserialize(deserializer)?
.into_iter()
.map(|tag| TagHash::from_bytes(tag.as_bytes()))
.collect::<HashSet<TagHash>>()
.collect::<IndexSet<TagHash>>()
.into())
}
pub fn tag_color_de<'de, D, T: std::convert::From<HashMap<TagHash, Color>>>(
pub fn tag_color_de<'de, D, T: std::convert::From<IndexMap<TagHash, Color>>>(
deserializer: D,
) -> std::result::Result<T, D::Error>
where
@ -64,7 +62,7 @@ where
C(Color),
}
Ok(<HashMap<String, _Color>>::deserialize(deserializer)?
Ok(<IndexMap<String, _Color>>::deserialize(deserializer)?
.into_iter()
.map(|(tag, color)| {
(
@ -75,7 +73,7 @@ where
},
)
})
.collect::<HashMap<TagHash, Color>>()
.collect::<IndexMap<TagHash, Color>>()
.into())
}
@ -93,7 +91,9 @@ impl DotAddressable for TagsSettings {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}

View file

@ -26,7 +26,7 @@ use melib::{Error, Result, ToggleFlag};
use super::{deserializers::non_empty_opt_string, DotAddressable, Themes};
/// Settings for terminal display
#[derive(Debug, Deserialize, Clone, Serialize)]
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct TerminalSettings {
/// light, dark
@ -34,6 +34,9 @@ pub struct TerminalSettings {
pub themes: Themes,
pub ascii_drawing: bool,
pub use_color: ToggleFlag,
/// Try forcing text presentations of symbols and emoji as much as possible.
/// Might not work on all non-text symbols and is experimental.
pub force_text_presentation: ToggleFlag,
/// Use mouse events. This will disable text selection, but you will be able
/// to resize some widgets.
/// Default: False
@ -55,10 +58,11 @@ pub struct TerminalSettings {
impl Default for TerminalSettings {
fn default() -> Self {
TerminalSettings {
Self {
theme: "dark".to_string(),
themes: Themes::default(),
ascii_drawing: false,
force_text_presentation: ToggleFlag::InternalVal(false),
use_color: ToggleFlag::InternalVal(true),
use_mouse: ToggleFlag::InternalVal(false),
mouse_flag: Some("🖱️ ".to_string()),
@ -70,15 +74,19 @@ impl Default for TerminalSettings {
}
impl TerminalSettings {
#[inline]
pub fn use_color(&self) -> bool {
/* Don't use color if
* - Either NO_COLOR is set and user hasn't explicitly set use_colors or
* - User has explicitly set use_colors to false
*/
!((std::env::var("NO_COLOR").is_ok()
// Don't use color if
// - Either NO_COLOR is set and user hasn't explicitly set use_colors or
// - User has explicitly set use_colors to false
!((std::env::var_os("NO_COLOR").is_some()
&& (self.use_color.is_false() || self.use_color.is_internal()))
|| (self.use_color.is_false() && !self.use_color.is_internal()))
}
pub fn use_text_presentation(&self) -> bool {
self.force_text_presentation.is_true() || !self.use_color()
}
}
impl DotAddressable for TerminalSettings {
@ -90,6 +98,7 @@ impl DotAddressable for TerminalSettings {
"theme" => self.theme.lookup(field, tail),
"themes" => Err(Error::new("unimplemented")),
"ascii_drawing" => self.ascii_drawing.lookup(field, tail),
"force_text_presentation" => self.force_text_presentation.lookup(field, tail),
"use_color" => self.use_color.lookup(field, tail),
"use_mouse" => self.use_mouse.lookup(field, tail),
"mouse_flag" => self.mouse_flag.lookup(field, tail),
@ -104,24 +113,103 @@ impl DotAddressable for TerminalSettings {
))),
}
}
None => Ok(toml::to_string(self).map_err(|err| err.to_string())?),
None => Ok(toml::Value::try_from(self)
.map_err(|err| err.to_string())?
.to_string()),
}
}
}
#[derive(Debug, Deserialize, Clone, Serialize)]
#[serde(untagged)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ProgressSpinnerSequence {
Integer(usize),
Custom {
frames: Vec<String>,
#[serde(default = "interval_ms_val")]
interval_ms: u64,
},
}
impl ProgressSpinnerSequence {
pub const fn interval_ms(&self) -> u64 {
match self {
Self::Integer(_) => interval_ms_val(),
Self::Custom {
frames: _,
interval_ms,
} => *interval_ms,
}
}
}
use serde::{Deserialize, Deserializer, Serialize, Serializer};
const fn interval_ms_val() -> u64 {
crate::utilities::ProgressSpinner::INTERVAL_MS
}
impl DotAddressable for ProgressSpinnerSequence {}
impl<'de> Deserialize<'de> for ProgressSpinnerSequence {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
enum Inner {
Integer(usize),
Frames(Vec<String>),
Custom {
frames: Vec<String>,
#[serde(default = "interval_ms_val")]
interval_ms: u64,
},
}
let s = <Inner>::deserialize(deserializer)?;
match s {
Inner::Integer(i) => Ok(Self::Integer(i)),
Inner::Frames(frames) => Ok(Self::Custom {
frames,
interval_ms: interval_ms_val(),
}),
Inner::Custom {
frames,
interval_ms,
} => Ok(Self::Custom {
frames,
interval_ms,
}),
}
}
}
impl Serialize for ProgressSpinnerSequence {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Integer(i) => serializer.serialize_i64(*i as i64),
Self::Custom {
frames,
interval_ms,
} => {
if *interval_ms == interval_ms_val() {
use serde::ser::SerializeSeq;
let mut seq = serializer.serialize_seq(Some(frames.len()))?;
for element in frames {
seq.serialize_element(element)?;
}
seq.end()
} else {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("frames", frames)?;
map.serialize_entry("interval_ms", interval_ms)?;
map.end()
}
}
}
}
}

384
meli/src/conf/tests.rs Normal file
View file

@ -0,0 +1,384 @@
//
// meli
//
// Copyright 2024 Emmanouil Pitsidianakis <manos@pitsidianak.is>
//
// This file is part of meli.
//
// meli is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// meli is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with meli. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: EUPL-1.2 OR GPL-3.0-or-later
use std::{
borrow::Cow,
fmt::Write as FmtWrite,
fs::{self, OpenOptions},
io::Write,
path::PathBuf,
};
use crate::{
conf::{themes::*, FileSettings},
terminal::Color,
};
pub struct ConfigFile {
pub path: PathBuf,
pub file: fs::File,
}
impl ConfigFile {
pub fn new(
content: &str,
dir: &tempfile::TempDir,
) -> std::result::Result<Self, std::io::Error> {
let mut filename = String::with_capacity(2 * 16);
for byte in melib::utils::random::random_u64().to_be_bytes() {
write!(&mut filename, "{:02X}", byte).unwrap();
}
let mut path = dir.path().to_path_buf();
path.push(&*filename);
let mut file = OpenOptions::new()
.create_new(true)
.append(true)
.open(&path)?;
file.write_all(content.as_bytes())?;
Ok(Self { path, file })
}
}
impl Drop for ConfigFile {
fn drop(&mut self) {
let _ = fs::remove_file(&self.path);
}
}
pub const TEST_CONFIG: &str = r#"
[accounts.account-name]
root_mailbox = "/path/to/root/mailbox"
format = "Maildir"
send_mail = 'false'
listing.index_style = "Conversations" # or [plain, threaded, compact]
identity="email@example.com"
display_name = "Name"
subscribed_mailboxes = ["INBOX", "INBOX/Sent", "INBOX/Drafts", "INBOX/Junk"]
# Set mailbox-specific settings
[accounts.account-name.mailboxes]
"INBOX" = { rename="Inbox" }
"drafts" = { rename="Drafts" }
"foobar-devel" = { ignore = true } # don't show notifications for this mailbox
# Setting up an mbox account
[accounts.mbox]
root_mailbox = "/var/mail/username"
format = "mbox"
send_mail = 'false'
listing.index_style = "Compact"
identity="username@hostname.local"
"#;
pub const EXTRA_CONFIG: &str = r#"
[accounts.mbox]
root_mailbox = "/"
format = "mbox"
send_mail = 'false'
index_style = "Compact"
identity="username@hostname.local"
"#;
pub const IMAP_CONFIG: &str = r#"
[accounts.imap]
root_mailbox = "INBOX"
format = "imap"
send_mail = 'false'
identity="username@example.com"
server_username = "null"
server_hostname = "example.com"
server_password_command = "false"
"#;
#[test]
fn test_conf_config_parse() {
let tempdir = tempfile::tempdir().unwrap();
let new_file = ConfigFile::new(TEST_CONFIG, &tempdir).unwrap();
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
assert_eq!(
err.summary.as_ref(),
"Configuration error (account-name): root_mailbox `/path/to/root/mailbox` is not a valid \
directory."
);
/* Test unrecognised configuration entries error */
let new_file = ConfigFile::new(EXTRA_CONFIG, &tempdir).unwrap();
let err = FileSettings::validate(new_file.path.clone(), true).unwrap_err();
assert_eq!(
err.summary.as_ref(),
"Unrecognised configuration values: {\"index_style\": \"Compact\"}"
);
/* Test IMAP config */
let new_file = ConfigFile::new(IMAP_CONFIG, &tempdir).unwrap();
FileSettings::validate(new_file.path.clone(), true).expect("could not parse IMAP config");
/* Test sample config */
// Sample config contains `crate::conf::composing::SendMail::Smtp` variant which
// only exists if meli is build with `smtp` feature.
if cfg!(feature = "smtp") {
let example_config = FileSettings::EXAMPLE_CONFIG.replace("\n#", "\n");
let re = regex::Regex::new(r#"root_mailbox\s*=\s*"[^"]*""#).unwrap();
let example_config = re.replace_all(
&example_config,
&format!(r#"root_mailbox = "{}""#, tempdir.path().to_str().unwrap()),
);
let new_file = ConfigFile::new(&example_config, &tempdir).unwrap();
let config = FileSettings::validate(new_file.path.clone(), true)
.expect("Could not parse example config!");
for (accname, acc) in config.accounts.iter() {
if !acc.extra.is_empty() {
panic!(
"In example config, account `{}` has unrecognised configuration entries: {:?}",
accname, acc.extra
);
}
}
}
if let Err(err) = tempdir.close() {
eprintln!("Could not cleanup tempdir: {}", err);
}
}
#[test]
fn test_conf_theme_parsing() {
/* MUST SUCCEED: default themes should be valid */
let def = Themes::default();
def.validate().unwrap();
/* MUST SUCCEED: new user theme `hunter2`, theme `dark` has user
* redefinitions */
const TEST_STR: &str = r#"[dark]
"mail.listing.tag_default" = { fg = "White", bg = "HotPink3" }
"mail.listing.attachment_flag" = { fg = "mail.listing.tag_default.bg" }
"mail.view.headers" = { bg = "mail.listing.tag_default.fg" }
["hunter2"]
"mail.view.body" = { fg = "Black", bg = "White"}"#;
let parsed: Themes = toml::from_str(TEST_STR).unwrap();
assert!(parsed.other_themes.contains_key("hunter2"));
assert_eq!(
unlink_bg(
&parsed.dark,
&ColorField::Bg,
&Cow::from("mail.listing.tag_default")
),
Color::Byte(132)
);
assert_eq!(
unlink_fg(
&parsed.dark,
&ColorField::Fg,
&Cow::from("mail.listing.attachment_flag")
),
Color::Byte(132)
);
assert_eq!(
unlink_bg(
&parsed.dark,
&ColorField::Bg,
&Cow::from("mail.view.headers")
),
Color::Byte(15), // White
);
parsed.validate().unwrap();
/* MUST FAIL: theme `dark` contains a cycle */
const HAS_CYCLE: &str = r#"[dark]
"mail.listing.compact.even" = { fg = "mail.listing.compact.odd" }
"mail.listing.compact.odd" = { fg = "mail.listing.compact.even" }
"#;
let parsed: Themes = toml::from_str(HAS_CYCLE).unwrap();
parsed.validate().unwrap_err();
/* MUST FAIL: theme `dark` contains an invalid key */
const HAS_INVALID_KEYS: &str = r#"[dark]
"asdfsafsa" = { fg = "Black" }
"#;
let parsed: std::result::Result<Themes, _> = toml::from_str(HAS_INVALID_KEYS);
parsed.unwrap_err();
/* MUST SUCCEED: alias $Jebediah resolves to a valid color */
const TEST_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"##;
let parsed: Themes = toml::from_str(TEST_ALIAS_STR).unwrap();
parsed.validate().unwrap();
assert_eq!(
unlink_fg(
&parsed.dark,
&ColorField::Fg,
&Cow::from("mail.listing.tag_default")
),
Color::Rgb(180, 218, 85)
);
/* MUST FAIL: Misspell color alias $Jebediah as $Jebedia */
const TEST_INVALID_ALIAS_STR: &str = r##"[dark]
color_aliases= { "Jebediah" = "#b4da55" }
"mail.listing.tag_default" = { fg = "$Jebedia" }
"##;
let parsed: Themes = toml::from_str(TEST_INVALID_ALIAS_STR).unwrap();
parsed.validate().unwrap_err();
/* MUST FAIL: Color alias $Jebediah is defined as itself */
const TEST_CYCLIC_ALIAS_STR: &str = r#"[dark]
color_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR).unwrap();
parsed.validate().unwrap_err();
/* MUST FAIL: Attr alias $Jebediah is defined as itself */
const TEST_CYCLIC_ALIAS_ATTR_STR: &str = r#"[dark]
attr_aliases= { "Jebediah" = "$Jebediah" }
"mail.listing.tag_default" = { attrs = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_ATTR_STR).unwrap();
parsed.validate().unwrap_err();
/* MUST FAIL: alias $Jebediah resolves to a cycle */
const TEST_CYCLIC_ALIAS_STR_2: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default" }
"mail.listing.tag_default" = { fg = "$Jebediah" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_2).unwrap();
parsed.validate().unwrap_err();
/* MUST SUCCEED: alias $Jebediah resolves to a key's field */
const TEST_CYCLIC_ALIAS_STR_3: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.bg" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"#;
let parsed: Themes = toml::from_str(TEST_CYCLIC_ALIAS_STR_3).unwrap();
parsed.validate().unwrap();
/* MUST FAIL: alias $Jebediah resolves to an invalid key */
const TEST_INVALID_LINK_KEY_FIELD_STR: &str = r#"[dark]
color_aliases= { "Jebediah" = "$JebediahJr", "JebediahJr" = "mail.listing.tag_default.attrs" }
"mail.listing.tag_default" = { fg = "$Jebediah", bg = "Black" }
"#;
let parsed: Themes = toml::from_str(TEST_INVALID_LINK_KEY_FIELD_STR).unwrap();
parsed.validate().unwrap_err();
}
#[test]
fn test_conf_theme_key_values() {
use std::{collections::VecDeque, fs::File, io::Read, path::PathBuf};
let mut rust_files: VecDeque<PathBuf> = VecDeque::new();
let mut dirs_queue: VecDeque<PathBuf> = VecDeque::new();
dirs_queue.push_back("src/".into());
let re_whitespace = regex::Regex::new(r"\s*").unwrap();
let re_conf = regex::Regex::new(r#"value\([&]?context,"([^"]*)""#).unwrap();
while let Some(dir) = dirs_queue.pop_front() {
for entry in std::fs::read_dir(&dir).unwrap() {
let entry = entry.unwrap();
let path = entry.path();
if path.is_dir() {
dirs_queue.push_back(path);
} else if path.extension().map(|os_s| os_s == "rs").unwrap_or(false) {
rust_files.push_back(path);
}
}
}
for file_path in rust_files {
let mut file = File::open(&file_path).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();
let content = re_whitespace.replace_all(&content, "");
for mat in re_conf.captures_iter(&content) {
let theme_key = &mat[1];
if !DEFAULT_KEYS.contains(&theme_key) {
panic!(
"Source file {} contains a hardcoded theme key str, {:?}, that is not \
included in the DEFAULT_KEYS table.",
file_path.display(),
theme_key
);
}
}
}
}
#[test]
fn test_conf_progress_spinner_sequence() {
use crate::{conf::terminal::ProgressSpinnerSequence, utilities::ProgressSpinner};
let int_0 = ProgressSpinnerSequence::Integer(5);
assert_eq!(
toml::Value::try_from(&int_0).unwrap(),
toml::Value::try_from(5).unwrap()
);
let frames = ProgressSpinnerSequence::Custom {
frames: vec![
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
],
interval_ms: ProgressSpinner::INTERVAL_MS,
};
assert_eq!(frames.interval_ms(), ProgressSpinner::INTERVAL_MS);
assert_eq!(
toml::Value::try_from(&frames).unwrap(),
toml::Value::try_from(["", "", "", "", "", "", "", ""]).unwrap()
);
let frames = ProgressSpinnerSequence::Custom {
frames: vec![
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
"".to_string(),
],
interval_ms: ProgressSpinner::INTERVAL_MS + 1,
};
assert_eq!(
toml::Value::try_from(&frames).unwrap(),
toml::Value::try_from(indexmap::indexmap! {
"frames" => toml::Value::try_from(["", "", "", "", "", "", "", ""]).unwrap(),
"interval_ms" => toml::Value::try_from(ProgressSpinner::INTERVAL_MS + 1).unwrap()
})
.unwrap()
);
assert_eq!(
toml::from_str::<ProgressSpinnerSequence>(
r#"frames = ["", "", "", "", "", "", "", ""]
interval_ms = 51"#
)
.unwrap(),
frames
);
assert_eq!(
toml::from_str::<indexmap::IndexMap<String, ProgressSpinnerSequence>>(
r#"sequence = { frames = ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"], interval_ms = 51 }"#
)
.unwrap(),
indexmap::indexmap! {
"sequence".to_string() => frames,
},
);
}

File diff suppressed because it is too large Load diff

View file

@ -19,13 +19,16 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::collections::HashMap;
use std::borrow::Cow;
use indexmap::IndexMap;
use melib::Card;
use crate::{
terminal::*, CellBuffer, Component, ComponentId, Context, Field, FormWidget, Key, StatusEvent,
ThemeAttribute, UIDialog, UIEvent,
terminal::*,
utilities::{FormButtonAction, FormWidget},
CellBuffer, Component, ComponentId, Context, Field, Key, StatusEvent, ThemeAttribute, UIDialog,
UIEvent,
};
#[derive(Debug)]
@ -41,7 +44,7 @@ pub struct ContactManager {
parent_id: Option<ComponentId>,
pub card: Card,
mode: ViewMode,
form: FormWidget<bool>,
form: FormWidget<FormButtonAction>,
pub account_pos: usize,
content: Screen<Virtual>,
theme_default: ThemeAttribute,
@ -59,14 +62,14 @@ impl std::fmt::Display for ContactManager {
impl ContactManager {
pub fn new(context: &Context) -> Self {
let theme_default: ThemeAttribute = crate::conf::value(context, "theme_default");
ContactManager {
Self {
id: ComponentId::default(),
parent_id: None,
card: Card::new(),
mode: ViewMode::Edit,
form: FormWidget::default(),
account_pos: 0,
content: Screen::<Virtual>::new(),
content: Screen::<Virtual>::new(theme_default),
theme_default,
dirty: true,
has_changes: false,
@ -78,26 +81,7 @@ impl ContactManager {
if !self.content.resize_with_context(100, 1, context) {
return;
}
let mut area = self.content.area();
let (x, _) = self.content.grid_mut().write_string(
"Last edited: ",
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
area,
None,
);
area = area.skip_cols(x);
let (x, y) = self.content.grid_mut().write_string(
&self.card.last_edited(),
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
area,
None,
);
area = area.skip(x, y);
let area = self.content.area();
if self.card.external_resource() {
self.mode = ViewMode::ReadOnly;
@ -108,16 +92,45 @@ impl ContactManager {
self.theme_default.attrs,
area,
None,
None,
);
} else {
let (x, _) = self.content.grid_mut().write_string(
"Last edited: ",
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
area,
None,
None,
);
self.content.grid_mut().write_string(
&self.card.last_edited(),
self.theme_default.fg,
self.theme_default.bg,
self.theme_default.attrs,
area.skip_cols(x),
None,
None,
);
}
self.form = FormWidget::new(
("Save".into(), true),
if self.card.external_resource() {
("Cancel(Esc)".into(), FormButtonAction::Cancel)
} else {
("Save".into(), FormButtonAction::Accept)
},
/* cursor_up_shortcut */ context.settings.shortcuts.general.scroll_up.clone(),
/* cursor_down_shortcut */
context.settings.shortcuts.general.scroll_down.clone(),
);
self.form.add_button(("Cancel(Esc)".into(), false));
if !self.card.external_resource() {
self.form
.add_button(("Export".into(), FormButtonAction::Other("Export")));
self.form
.add_button(("Cancel(Esc)".into(), FormButtonAction::Cancel));
}
self.form
.push(("NAME".into(), self.card.name().to_string()));
self.form.push((
@ -151,15 +164,12 @@ impl Component for ContactManager {
if self.is_dirty() {
grid.clear_area(area, self.theme_default);
grid.copy_area(self.content.grid(), area.skip_rows(2), self.content.area());
grid.copy_area(self.content.grid(), area, self.content.area());
self.dirty = false;
}
self.form.draw(
grid,
area.skip_rows(2 + self.content.area().height()),
context,
);
self.form
.draw(grid, area.skip_rows(self.content.area().height()), context);
if let ViewMode::Discard(ref mut selector) = self.mode {
/* Let user choose whether to quit with/without saving or cancel */
selector.draw(grid, area, context);
@ -198,9 +208,9 @@ impl Component for ContactManager {
if self.form.process_event(event, context) {
match self.form.buttons_result() {
None => {}
Some(true) => {
let fields = std::mem::take(&mut self.form).collect().unwrap();
let fields: HashMap<String, String> = fields
Some(FormButtonAction::Accept) => {
let fields = std::mem::take(&mut self.form).collect();
let fields: IndexMap<String, String> = fields
.into_iter()
.map(|(s, v)| {
(
@ -215,26 +225,53 @@ impl Component for ContactManager {
let mut new_card = Card::from(fields);
new_card.set_id(*self.card.id());
context.accounts[self.account_pos]
.address_book
.contacts
.add_card(new_card);
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage("Saved.".into()),
));
self.unrealize(context);
}
Some(false) => {
Some(FormButtonAction::Cancel) => {
self.unrealize(context);
}
Some(FormButtonAction::Other("Export")) => {
let card = if self.has_changes {
let fields = self.form.clone().collect();
let fields: IndexMap<String, String> = fields
.into_iter()
.map(|(s, v)| {
(
s.to_string(),
match v {
Field::Text(v) => v.as_str().to_string(),
Field::Choice(mut v, c, _) => {
v.remove(c).to_string()
}
},
)
})
.collect();
let mut card = Card::from(fields);
card.set_id(*self.card.id());
Cow::Owned(card)
} else {
Cow::Borrowed(&self.card)
};
super::export_to_vcard(&card, self.account_pos, context);
return true;
}
Some(FormButtonAction::Reset | FormButtonAction::Other(_)) => {}
}
self.set_dirty(true);
if let UIEvent::InsertInput(_) = event {
if matches!(event, UIEvent::InsertInput(_)) {
self.has_changes = true;
}
return true;
}
}
ViewMode::ReadOnly => {
if let &mut UIEvent::Input(Key::Esc) = event {
if matches!(event, UIEvent::Input(Key::Esc)) {
if self.can_quit_cleanly(context) {
self.unrealize(context);
}

View file

@ -21,7 +21,7 @@
use std::cmp;
use melib::{backends::AccountHash, text_processing::TextProcessing, Card, CardId, Draft};
use melib::{backends::AccountHash, text::TextProcessing, Card, CardId, Draft};
use crate::{
conf, contacts::editor::ContactManager, shortcut, terminal::*, Action::Tab, Component,
@ -88,7 +88,8 @@ impl ContactList {
index: i,
})
.collect();
ContactList {
let theme_default = crate::conf::value(context, "theme_default");
Self {
accounts,
cursor_pos: 0,
new_cursor_pos: 0,
@ -96,8 +97,8 @@ impl ContactList {
account_pos: 0,
id_positions: Vec::new(),
mode: ViewMode::List,
data_columns: DataColumns::default(),
theme_default: crate::conf::value(context, "theme_default"),
data_columns: DataColumns::new(theme_default),
theme_default,
highlight_theme: crate::conf::value(context, "highlight"),
initialized: false,
dirty: true,
@ -112,26 +113,26 @@ impl ContactList {
}
pub fn for_account(pos: usize, context: &Context) -> Self {
ContactList {
Self {
account_pos: pos,
..Self::new(context)
}
}
fn initialize(&mut self, context: &mut Context) {
fn initialize(&mut self, context: &Context) {
self.data_columns.clear();
let account = &context.accounts[self.account_pos];
let book = &account.address_book;
self.length = book.len();
let contacts = &account.contacts;
self.length = contacts.len();
self.id_positions.clear();
if self.id_positions.capacity() < book.len() {
self.id_positions.reserve(book.len());
if self.id_positions.capacity() < contacts.len() {
self.id_positions.reserve(contacts.len());
}
self.dirty = true;
let mut min_width = ("Name".len(), "E-mail".len(), 0, "external".len(), 0, 0);
for c in book.values() {
for c in contacts.values() {
/* name */
let name = c.name().split_graphemes().len();
if name > 0 {
@ -163,8 +164,8 @@ impl ContactList {
);
let account = &context.accounts[self.account_pos];
let book = &account.address_book;
let mut book_values = book.values().collect::<Vec<&Card>>();
let contacts = &account.contacts;
let mut book_values = contacts.values().collect::<Vec<&Card>>();
book_values.sort_unstable_by_key(|c| c.name());
for (idx, c) in book_values.iter().enumerate() {
self.id_positions.push(*c.id());
@ -178,6 +179,7 @@ impl ContactList {
self.theme_default.attrs,
area,
None,
None,
)
};
@ -190,6 +192,7 @@ impl ContactList {
self.theme_default.attrs,
area,
None,
None,
)
};
@ -202,6 +205,7 @@ impl ContactList {
self.theme_default.attrs,
area,
None,
None,
)
};
@ -218,6 +222,7 @@ impl ContactList {
self.theme_default.attrs,
area,
None,
None,
)
};
}
@ -234,19 +239,19 @@ impl ContactList {
self.theme_default.attrs,
area,
None,
None,
);
}
}
}
fn highlight_line(&mut self, grid: &mut CellBuffer, area: Area, idx: usize) {
fn highlight_line(&self, grid: &mut CellBuffer, area: Area, idx: usize) {
/* Reset previously highlighted line */
let mut theme = if idx == self.new_cursor_pos {
self.highlight_theme
} else {
self.theme_default
};
theme.fg = self.theme_default.fg;
if !grid.use_color {
theme.attrs |= Attr::REVERSE;
}
@ -274,7 +279,7 @@ impl ContactList {
grid: &mut CellBuffer,
area: Area,
a: &AccountMenuEntry,
context: &mut Context,
context: &Context,
) {
let width = area.width();
let must_highlight_account: bool = self.account_pos == a.index;
@ -289,7 +294,7 @@ impl ContactList {
};
grid.change_theme(area, account_attrs);
let s = format!(" [{}]", context.accounts[a.index].address_book.len());
let s = format!(" [{}]", context.accounts[a.index].contacts.len());
/* Print account name */
grid.write_string(
&a.name,
@ -298,6 +303,7 @@ impl ContactList {
account_attrs.attrs,
area,
None,
None,
);
grid.write_string(
&s,
@ -306,6 +312,7 @@ impl ContactList {
account_attrs.attrs,
area.skip_cols(area.width().saturating_sub(s.len())),
None,
None,
);
if a.name.grapheme_len() + s.len() > width + 1 {
@ -316,6 +323,7 @@ impl ContactList {
account_attrs.attrs,
area.skip_cols(area.width().saturating_sub(s.len() + 1)),
None,
None,
);
}
}
@ -449,6 +457,7 @@ impl ContactList {
.skip_cols(x)
.take_cols(x + (self.data_columns.widths[i])),
None,
None,
);
x += self.data_columns.widths[i] + 2; // + SEPARATOR
@ -573,7 +582,6 @@ impl Component for ContactList {
)));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["edit_contact"]) =>
{
@ -581,8 +589,8 @@ impl Component for ContactList {
return true;
}
let account = &mut context.accounts[self.account_pos];
let book = &mut account.address_book;
let card = book[&self.id_positions[self.cursor_pos]].clone();
let contacts = &mut account.contacts;
let card = contacts[&self.id_positions[self.cursor_pos]].clone();
let mut manager = Box::new(ContactManager::new(context));
manager.set_parent_id(self.id);
manager.card = card;
@ -596,6 +604,20 @@ impl Component for ContactList {
)));
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["export_contact"]) =>
{
if self.length == 0 {
return true;
}
let card = {
let account = &context.accounts[self.account_pos];
let contacts = &account.contacts;
contacts[&self.id_positions[self.cursor_pos]].clone()
};
super::export_to_vcard(&card, self.account_pos, context);
return true;
}
UIEvent::Input(ref key)
if shortcut!(key == shortcuts[Shortcuts::CONTACT_LIST]["mail_contact"]) =>
{
@ -604,8 +626,8 @@ impl Component for ContactList {
}
let account = &context.accounts[self.account_pos];
let account_hash = account.hash();
let book = &account.address_book;
let card = &book[&self.id_positions[self.cursor_pos]];
let contacts = &account.contacts;
let card = &contacts[&self.id_positions[self.cursor_pos]];
let mut draft: Draft = Draft::default();
*draft.headers_mut().get_mut("To").unwrap() =
format!("{} <{}>", &card.name(), &card.email());
@ -627,7 +649,7 @@ impl Component for ContactList {
}
// [ref:TODO]: add a confirmation dialog?
context.accounts[self.account_pos]
.address_book
.contacts
.remove_card(self.id_positions[self.cursor_pos]);
self.initialized = false;
self.set_dirty(true);
@ -715,7 +737,7 @@ impl Component for ContactList {
self.menu_visibility = !self.menu_visibility;
self.set_dirty(true);
}
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Alt(''))
UIEvent::Input(Key::Esc) | UIEvent::Input(Key::Char('\x1b'))
if !self.cmd_buf.is_empty() =>
{
self.cmd_buf.clear();
@ -838,6 +860,25 @@ impl Component for ContactList {
self.movement = Some(PageMovement::End);
return true;
}
UIEvent::Input(ref key)
if context
.settings
.shortcuts
.contact_list
.commands
.iter()
.any(|cmd| {
if cmd.shortcut == *key {
for cmd in &cmd.command {
context.replies.push_back(UIEvent::Command(cmd.to_string()));
}
return true;
}
false
}) =>
{
return true;
}
_ => {}
}
}
@ -895,7 +936,7 @@ impl Component for ContactList {
fn status(&self, context: &Context) -> String {
format!(
"{} entries",
context.accounts[self.account_pos].address_book.len()
context.accounts[self.account_pos].contacts.len()
)
}
}

View file

@ -19,5 +19,57 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use melib::Card;
use crate::{
types::{sanitize_filename, File, NotificationType, UIEvent},
Context,
};
pub mod editor;
pub mod list;
pub fn export_to_vcard(card: &Card, account_pos: usize, context: &mut Context) {
let mut output_dir = context.accounts[account_pos]
.settings
.account
.vcard_folder()
.map(|s| std::path::Path::new(s).to_path_buf());
let filename = sanitize_filename(format!(
"{prefix}{name}{suffix}{space}{additionalname}",
prefix = card.name_prefix(),
name = card.name(),
suffix = card.name_suffix(),
space = if card.additionalname().trim().is_empty() {
""
} else {
" "
},
additionalname = card.additionalname()
));
let res = File::create_temp_file(
card.to_vcard_string().as_bytes(),
filename.as_deref(),
output_dir.as_mut(),
Some("vcf"),
false,
);
match res {
Ok(f) => {
context.replies.push_back(UIEvent::Notification {
title: Some("Exported .vcf".into()),
body: format!("Exported contact to vcard file to\n{}", f.path().display()).into(),
kind: Some(NotificationType::Info),
source: None,
});
}
Err(err) => {
context.replies.push_back(UIEvent::Notification {
title: Some("Could not export contact.".into()),
body: err.to_string().into(),
kind: Some(NotificationType::Error(err.kind)),
source: Some(err),
});
}
}
}

View file

@ -26,7 +26,10 @@ use std::{
future::Future,
iter,
panic::catch_unwind,
sync::{Arc, Mutex},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
thread,
time::Duration,
};
@ -42,8 +45,44 @@ use melib::{log, smol, utils::datetime, uuid::Uuid, UnixTimestamp};
use crate::types::{StatusEvent, ThreadEvent, UIEvent};
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
pub enum IsAsync {
Async,
Blocking,
}
type AsyncTask = async_task::Runnable;
#[derive(Clone, Debug)]
struct FinishedTimestamp(Arc<Mutex<UnixTimestamp>>);
impl FinishedTimestamp {
fn finished(&self) -> Option<UnixTimestamp> {
match self.0.lock() {
Ok(v) if *v == 0 => None,
Ok(v) => Some(*v),
Err(poison) => {
let mut guard = poison.into_inner();
if *guard == 0 {
*guard = UnixTimestamp::default();
}
Some(*guard)
}
}
}
fn set_finished(&self, new_value: Option<UnixTimestamp>) {
let new_value = new_value.unwrap_or_default();
match self.0.lock() {
Ok(mut f) => *f = new_value,
Err(poison) => {
let mut guard = poison.into_inner();
*guard = new_value;
}
}
}
}
fn find_task(
local: &Worker<MeliTask>,
global: &Injector<MeliTask>,
@ -110,15 +149,41 @@ pub struct MeliTask {
timer: bool,
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
/// A spawned future's metadata for book-keeping.
pub struct JobMetadata {
pub id: JobId,
pub desc: Cow<'static, str>,
pub timer: bool,
pub started: UnixTimestamp,
pub finished: Option<UnixTimestamp>,
pub succeeded: bool,
id: JobId,
desc: Cow<'static, str>,
timer: bool,
started: UnixTimestamp,
finished: FinishedTimestamp,
succeeded: bool,
}
impl JobMetadata {
pub fn id(&self) -> &JobId {
&self.id
}
pub fn description(&self) -> &str {
&self.desc
}
pub fn is_timer(&self) -> bool {
self.timer
}
pub fn started(&self) -> UnixTimestamp {
self.started
}
pub fn finished(&self) -> Option<UnixTimestamp> {
self.finished.finished()
}
pub fn succeeded(&self) -> bool {
self.succeeded
}
}
#[derive(Debug)]
@ -139,7 +204,7 @@ struct TimerPrivate {
value: Duration,
active: bool,
handle: Option<async_task::Task<()>>,
cancel: Arc<Mutex<bool>>,
cancel: Arc<AtomicBool>,
}
#[derive(Debug)]
@ -176,7 +241,7 @@ impl JobExecutor {
/// A queue that holds scheduled tasks.
pub fn new(sender: Sender<ThreadEvent>) -> Self {
// Create a queue.
let mut ret = JobExecutor {
let mut ret = Self {
global_queue: Arc::new(Injector::new()),
workers: vec![],
parkers: vec![],
@ -185,7 +250,10 @@ impl JobExecutor {
jobs: Arc::new(Mutex::new(IndexMap::default())),
};
let mut workers = vec![];
for _ in 0..num_cpus::get().max(1) {
for _ in 0..std::thread::available_parallelism()
.map(Into::into)
.unwrap_or(1)
{
let new_worker = Worker::new_fifo();
ret.workers.push(new_worker.stealer());
let p = Parker::new();
@ -234,7 +302,27 @@ impl JobExecutor {
}
/// Spawns a future with a generic return value `R`
pub fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
#[inline(always)]
pub fn spawn<F, R>(
&self,
desc: Cow<'static, str>,
future: F,
is_async: IsAsync,
) -> JoinHandle<R>
where
F: Future<Output = R> + Send + 'static,
R: Send + 'static,
{
if matches!(is_async, IsAsync::Async) {
self.spawn_specialized(desc, future)
} else {
self.spawn_blocking(desc, future)
}
}
/// Spawns a future with a generic return value `R`
#[inline(always)]
fn spawn_specialized<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
where
F: Future<Output = R> + Send + 'static,
R: Send + 'static,
@ -243,8 +331,10 @@ impl JobExecutor {
let finished_sender = self.sender.clone();
let job_id = JobId::new();
let injector = self.global_queue.clone();
let cancel = Arc::new(Mutex::new(false));
let cancel2 = cancel.clone();
// We do not use `AtomicU64` because it's not portable, so ignore the lint.
#[allow(clippy::mutex_integer)]
let finished = FinishedTimestamp(Arc::new(Mutex::new(0)));
let cancel = Arc::new(AtomicBool::new(false));
self.jobs.lock().unwrap().insert(
job_id,
@ -252,34 +342,41 @@ impl JobExecutor {
id: job_id,
desc: desc.clone(),
started: datetime::now(),
finished: None,
finished: finished.clone(),
succeeded: true,
timer: false,
},
);
// Create a task and schedule it for execution.
let (handle, task) = async_task::spawn(
async move {
let res = future.await;
let _ = sender.send(res);
finished_sender
.send(ThreadEvent::JobFinished(job_id))
.unwrap();
},
move |task| {
if *cancel.lock().unwrap() {
return;
}
let desc = desc.clone();
injector.push(MeliTask {
task,
id: job_id,
desc,
timer: false,
})
},
);
let (handle, task) = {
let cancel = cancel.clone();
let finished = finished.clone();
async_task::spawn(
async move {
let res = future.await;
let _ = sender.send(res);
if let Ok(mut guard) = finished.0.lock() {
*guard = datetime::now();
}
finished_sender
.send(ThreadEvent::JobFinished(job_id))
.unwrap();
},
move |task| {
if cancel.load(Ordering::SeqCst) {
return;
}
let desc = desc.clone();
injector.push(MeliTask {
task,
id: job_id,
desc,
timer: false,
})
},
)
};
handle.schedule();
for unparker in self.parkers.iter() {
unparker.unpark();
@ -287,7 +384,8 @@ impl JobExecutor {
JoinHandle {
task: Arc::new(Mutex::new(Some(task))),
cancel: cancel2,
cancel,
finished,
chan: receiver,
job_id,
}
@ -295,7 +393,8 @@ impl JobExecutor {
/// Spawns a future with a generic return value `R` that might block on a
/// new thread
pub fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
#[inline(always)]
fn spawn_blocking<F, R>(&self, desc: Cow<'static, str>, future: F) -> JoinHandle<R>
where
F: Future<Output = R> + Send + 'static,
R: Send + 'static,
@ -306,10 +405,10 @@ impl JobExecutor {
)
}
pub fn create_timer(self: Arc<JobExecutor>, interval: Duration, value: Duration) -> Timer {
pub fn create_timer(self: Arc<Self>, interval: Duration, value: Duration) -> Timer {
let timer = TimerPrivate {
interval,
cancel: Arc::new(Mutex::new(false)),
cancel: Arc::new(AtomicBool::new(false)),
value,
active: true,
handle: None,
@ -337,46 +436,48 @@ impl JobExecutor {
let sender = self.sender.clone();
let injector = self.global_queue.clone();
let timers = self.timers.clone();
let cancel = Arc::new(Mutex::new(false));
let cancel2 = cancel.clone();
let (task, handle) = async_task::spawn(
async move {
let mut value = value;
loop {
smol::Timer::after(value).await;
sender
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
.unwrap();
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
None
} else if timer.active {
Some(timer.interval)
let cancel = Arc::new(AtomicBool::new(false));
let (task, handle) = {
let cancel = cancel.clone();
async_task::spawn(
async move {
let mut value = value;
loop {
smol::Timer::after(value).await;
sender
.send(ThreadEvent::UIEvent(UIEvent::Timer(id)))
.unwrap();
if let Some(interval) = timers.lock().unwrap().get(&id).and_then(|timer| {
if timer.interval.as_millis() == 0 && timer.interval.as_secs() == 0 {
None
} else if timer.active {
Some(timer.interval)
} else {
None
}
}) {
value = interval;
} else {
None
break;
}
}) {
value = interval;
} else {
break;
}
}
},
move |task| {
if *cancel.lock().unwrap() {
return;
}
injector.push(MeliTask {
task,
id: job_id,
desc: "timer".into(),
timer: true,
})
},
);
},
move |task| {
if cancel.load(Ordering::SeqCst) {
return;
}
injector.push(MeliTask {
task,
id: job_id,
desc: Cow::Borrowed("timer"),
timer: true,
})
},
)
};
self.timers.lock().unwrap().entry(id).and_modify(|timer| {
timer.handle = Some(handle);
timer.cancel = cancel2;
timer.cancel = cancel;
timer.active = true;
});
task.schedule();
@ -389,7 +490,7 @@ impl JobExecutor {
let mut timers_lck = self.timers.lock().unwrap();
if let Some(timer) = timers_lck.get_mut(&id) {
timer.active = false;
*timer.cancel.lock().unwrap() = true;
timer.cancel.store(true, Ordering::SeqCst);
}
}
@ -402,7 +503,7 @@ impl JobExecutor {
pub fn set_job_finished(&self, id: JobId) {
self.jobs.lock().unwrap().entry(id).and_modify(|entry| {
entry.finished = Some(datetime::now());
entry.finished.set_finished(Some(datetime::now()));
});
}
@ -415,25 +516,34 @@ impl JobExecutor {
pub type JobChannel<T> = oneshot::Receiver<T>;
/// JoinHandle for the future that allows us to cancel the task.
/// `JoinHandle` for the future that allows us to cancel the task.
#[derive(Debug)]
pub struct JoinHandle<T> {
pub task: Arc<Mutex<Option<async_task::Task<()>>>>,
pub chan: JobChannel<T>,
pub cancel: Arc<Mutex<bool>>,
pub cancel: Arc<AtomicBool>,
finished: FinishedTimestamp,
pub job_id: JobId,
}
impl<T> JoinHandle<T> {
pub fn cancel(&self) -> Option<StatusEvent> {
let mut lck = self.cancel.lock().unwrap();
if !*lck {
*lck = true;
let was_active = self.cancel.swap(true, Ordering::SeqCst);
if was_active {
self.finished.set_finished(Some(datetime::now()));
Some(StatusEvent::JobCanceled(self.job_id))
} else {
None
}
}
pub fn finished(&self) -> Option<UnixTimestamp> {
self.finished.finished()
}
pub fn is_canceled(&self) -> bool {
self.cancel.load(Ordering::SeqCst)
}
}
impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
@ -442,17 +552,8 @@ impl<T> std::cmp::PartialEq<JobId> for JoinHandle<T> {
}
}
/*
use std::pin::Pin;
use std::task::{Context, Poll};
impl Future for JoinHandle {
type Output = Result<()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match Pin::new(&mut self.inner).poll(cx) {
Poll::Pending => Poll::Pending,
Poll::Ready(output) => Poll::Ready(output.expect("task failed")),
}
impl<T> Drop for JoinHandle<T> {
fn drop(&mut self) {
_ = self.cancel();
}
}
*/

View file

@ -19,6 +19,60 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
#![deny(
rustdoc::redundant_explicit_links,
unsafe_op_in_unsafe_fn,
/* groups */
clippy::correctness,
clippy::suspicious,
clippy::complexity,
clippy::perf,
clippy::cargo,
clippy::nursery,
clippy::style,
/* restriction */
clippy::dbg_macro,
clippy::rc_buffer,
clippy::as_underscore,
clippy::assertions_on_result_states,
/* rustdoc */
rustdoc::broken_intra_doc_links,
/* pedantic */
//clippy::cast_lossless,
//clippy::cast_possible_wrap,
//clippy::ptr_as_ptr,
clippy::doc_markdown,
clippy::expect_fun_call,
clippy::or_fun_call,
clippy::bool_to_int_with_if,
clippy::borrow_as_ptr,
clippy::cast_ptr_alignment,
clippy::large_futures,
clippy::waker_clone_wake,
clippy::unused_enumerate_index,
clippy::unnecessary_fallible_conversions,
clippy::struct_field_names,
clippy::manual_hash_one,
clippy::into_iter_without_iter,
)]
#![allow(
clippy::option_if_let_else,
clippy::missing_const_for_fn,
clippy::significant_drop_tightening,
clippy::multiple_crate_versions,
clippy::significant_drop_in_scrutinee,
clippy::cognitive_complexity,
clippy::manual_clamp
)]
/* Source Code Annotation Tags:
*
* Global tags (in tagref format <https://github.com/stepchowfun/tagref>) for source code
* annotation:
*
* - tags from melib/src/lib.rs.
* - [tag:hardcoded_color_value] Replace hardcoded color values with user configurable ones.
*/
//!
//! This crate contains the frontend stuff of the application. The application
//! entry way on `src/bin.rs` creates an event loop and passes input to a
@ -38,7 +92,6 @@ pub use melib::uuid;
pub extern crate bitflags;
pub extern crate serde_json;
#[macro_use]
pub extern crate smallvec;
pub extern crate termion;
@ -49,11 +102,14 @@ static GLOBAL: System = System;
pub extern crate melib;
pub use melib::{
error::*, log, AccountHash, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel, Mail, Mailbox,
MailboxHash, ThreadHash, ToggleFlag,
error::*, log, AccountHash, ActionFlag, Envelope, EnvelopeHash, EnvelopeRef, Flag, LogLevel,
Mail, Mailbox, MailboxHash, ThreadHash, ToggleFlag,
};
pub mod args;
#[cfg(feature = "cli-docs")]
pub mod manpages;
pub mod signal_handlers;
pub mod subcommands;
#[macro_use]
@ -85,19 +141,17 @@ pub use crate::mail::*;
pub mod notifications;
pub mod mailbox_management;
pub use mailbox_management::*;
pub mod manage;
pub use manage::*;
pub mod jobs_view;
pub use jobs_view::*;
#[cfg(feature = "svgscreenshot")]
pub mod svg;
// #[cfg(feature = "svgscreenshot")]
// pub mod svg;
#[macro_use]
pub mod conf;
pub use crate::conf::{
DotAddressable, IndexStyle, SearchBackend, Settings, Shortcuts, ThemeAttribute,
data_types::{IndexStyle, SearchBackend},
DotAddressable, Settings, Shortcuts, ThemeAttribute,
};
#[cfg(feature = "sqlite3")]
@ -108,3 +162,7 @@ pub mod mailcap;
pub mod accounts;
pub use self::accounts::Account;
pub mod patch_retrieve;
pub mod version_migrations;

View file

@ -21,18 +21,21 @@
//! Entities that handle Mail specific functions.
use std::{future::Future, pin::Pin};
use indexmap::IndexMap;
use melib::{
backends::{AccountHash, Mailbox, MailboxHash},
email::{attachment_types::*, attachments::*},
text::{TextProcessing, Truncate},
thread::ThreadNodeHash,
};
use uuid::Uuid;
use super::*;
use crate::{
melib::text_processing::{TextProcessing, Truncate},
uuid::Uuid,
};
pub type AttachmentBoxFuture = Pin<Box<dyn Future<Output = Result<AttachmentBuilder>> + Send>>;
pub type AttachmentFilterBox = Box<dyn FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send>;
pub mod listing;
pub use crate::listing::*;

File diff suppressed because it is too large Load diff

View file

@ -21,7 +21,7 @@
use super::*;
#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum EditAttachmentCursor {
AttachmentNo(usize),
#[default]
@ -32,7 +32,7 @@ pub enum EditAttachmentCursor {
pub enum EditAttachmentMode {
Overview,
Edit {
inner: Box<FormWidget<FormButtonActions>>,
inner: Box<FormWidget<FormButtonAction>>,
no: usize,
},
}
@ -42,7 +42,7 @@ pub struct EditAttachments {
/// For shortcut setting retrieval.
pub account_hash: Option<AccountHash>,
pub mode: EditAttachmentMode,
pub buttons: ButtonWidget<FormButtonActions>,
pub buttons: ButtonWidget<FormButtonAction>,
pub cursor: EditAttachmentCursor,
pub dirty: bool,
pub id: ComponentId,
@ -50,11 +50,11 @@ pub struct EditAttachments {
impl EditAttachments {
pub fn new(account_hash: Option<AccountHash>) -> Self {
//ButtonWidget::new(("Add".into(), FormButtonActions::Other("add")));
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonActions::Cancel));
//ButtonWidget::new(("Add".into(), FormButtonAction::Other("add")));
let mut buttons = ButtonWidget::new(("Go Back".into(), FormButtonAction::Cancel));
buttons.set_focus(true);
buttons.set_cursor(1);
EditAttachments {
Self {
account_hash,
mode: EditAttachmentMode::Overview,
buttons,
@ -70,7 +70,7 @@ impl EditAttachmentsRefMut<'_, '_> {
&self,
no: usize,
context: &Context,
) -> Option<Box<FormWidget<FormButtonActions>>> {
) -> Option<Box<FormWidget<FormButtonAction>>> {
if no >= self.draft.attachments().len() {
return None;
}
@ -79,7 +79,7 @@ impl EditAttachmentsRefMut<'_, '_> {
let shortcuts = self.shortcuts(context);
let mut ret = FormWidget::new(
("Save".into(), FormButtonActions::Accept),
("Save".into(), FormButtonAction::Accept),
/* cursor_up_shortcut */
shortcuts
.get(Shortcuts::COMPOSING)
@ -92,8 +92,8 @@ impl EditAttachmentsRefMut<'_, '_> {
.unwrap_or_else(|| context.settings.shortcuts.composing.scroll_down.clone()),
);
ret.add_button(("Reset".into(), FormButtonActions::Reset));
ret.add_button(("Cancel".into(), FormButtonActions::Cancel));
ret.add_button(("Reset".into(), FormButtonAction::Reset));
ret.add_button(("Cancel".into(), FormButtonAction::Cancel));
ret.push(("Filename".into(), filename.unwrap_or_default().to_string()));
ret.push(("Mime type".into(), mime_type.to_string()));
Some(Box::new(ret))
@ -132,6 +132,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
theme_default.attrs,
area,
None,
None,
);
} else {
grid.write_string(
@ -141,6 +142,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
theme_default.attrs,
area,
None,
None,
);
for (i, a) in self.draft.attachments().iter().enumerate() {
let bg = if let EditAttachmentCursor::AttachmentNo(u) = self.inner.cursor {
@ -174,6 +176,7 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
theme_default.attrs,
area.skip(2, 2 + i),
None,
None,
);
}
}
@ -195,10 +198,10 @@ impl Component for EditAttachmentsRefMut<'_, '_> {
{
if inner.process_event(event, context) {
match inner.buttons_result() {
Some(FormButtonActions::Accept) | Some(FormButtonActions::Cancel) => {
Some(FormButtonAction::Accept) | Some(FormButtonAction::Cancel) => {
self.inner.mode = EditAttachmentMode::Overview;
}
Some(FormButtonActions::Reset) => {
Some(FormButtonAction::Reset) => {
let no = *no;
if let Some(inner) = self.new_edit_widget(no, context) {
self.inner.mode = EditAttachmentMode::Edit { inner, no };

View file

@ -21,15 +21,94 @@
use super::*;
type KeylistJoinHandle = JoinHandle<Result<Vec<melib::gpgme::Key>>>;
#[derive(Debug)]
pub enum KeySelection {
LoadingKeys {
handle: JoinHandle<Result<Vec<melib::gpgme::Key>>>,
progress_spinner: ProgressSpinner,
pub struct KeySelectionLoading {
handles: (KeylistJoinHandle, Vec<KeylistJoinHandle>),
progress_spinner: ProgressSpinner,
secret: bool,
local: bool,
patterns: (String, Vec<String>),
allow_remote_lookup: ActionFlag,
}
impl KeySelectionLoading {
pub fn new(
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
patterns: (String, Vec<String>),
allow_remote_lookup: ActionFlag,
context: &Context,
) -> Result<Self> {
use melib::gpgme::{self, *};
let mut ctx = gpgme::Context::new()?;
if local {
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
} else {
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
}
let (pattern, other_patterns) = patterns;
let main_job = ctx.keylist(secret, Some(pattern.clone()))?;
let main_handle = context.main_loop_handler.job_executor.spawn(
"gpg::keylist".into(),
main_job,
IsAsync::Blocking,
);
let other_handles = other_patterns
.iter()
.map(|pattern| {
let job = ctx.keylist(secret, Some(pattern.clone()))?;
Ok(context.main_loop_handler.job_executor.spawn(
"gpg::keylist".into(),
job,
IsAsync::Blocking,
))
})
.collect::<Result<Vec<KeylistJoinHandle>>>()?;
let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start();
Ok(Self {
handles: (main_handle, other_handles),
secret,
local,
patterns: (pattern, other_patterns),
allow_remote_lookup,
progress_spinner,
})
}
pub fn merge(&mut self, rhs: Self) {
let Self {
handles: (_, ref mut other_handles),
secret: _,
local: _,
patterns: (_, ref mut other_patterns),
allow_remote_lookup: _,
progress_spinner: _,
} = self;
let Self {
handles: (rhs_handle, rhs_other_handles),
patterns: (rhs_pattern, rhs_other_patterns),
secret: _,
local: _,
allow_remote_lookup: _,
progress_spinner: _,
} = rhs;
other_handles.push(rhs_handle);
other_handles.extend(rhs_other_handles);
other_patterns.push(rhs_pattern);
other_patterns.extend(rhs_other_patterns);
}
}
#[derive(Debug)]
pub enum KeySelection {
Loading {
inner: KeySelectionLoading,
/// Accumulate results from intermediate results (i.e. not the main
/// pattern)
keys_accumulator: Vec<melib::gpgme::Key>,
},
Error {
id: ComponentId,
@ -41,53 +120,33 @@ pub enum KeySelection {
},
}
impl From<KeySelectionLoading> for KeySelection {
fn from(inner: KeySelectionLoading) -> Self {
Self::Loading {
inner,
keys_accumulator: vec![],
}
}
}
impl std::fmt::Display for KeySelection {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "select pgp keys")
}
}
impl KeySelection {
pub fn new(
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ToggleFlag,
context: &mut Context,
) -> Result<Self> {
use melib::gpgme::*;
let mut ctx = Context::new()?;
if local {
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
} else {
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
}
let job = ctx.keylist(secret, Some(pattern.clone()))?;
let handle = context
.main_loop_handler
.job_executor
.spawn_specialized("gpg::keylist".into(), job);
let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start();
Ok(KeySelection::LoadingKeys {
handle,
secret,
local,
pattern,
allow_remote_lookup,
progress_spinner,
})
}
}
impl Component for KeySelection {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
..
Self::Loading {
inner:
KeySelectionLoading {
ref mut progress_spinner,
..
},
keys_accumulator: _,
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
KeySelection::Error { ref err, .. } => {
Self::Error { ref err, .. } => {
let theme_default = crate::conf::value(context, "theme_default");
grid.write_string(
&err.to_string(),
@ -95,25 +154,41 @@ impl Component for KeySelection {
theme_default.bg,
theme_default.attrs,
area.center_inside((15, 2)),
None,
Some(0),
);
}
KeySelection::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
Self::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
}
}
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
ref mut handle,
secret,
local,
ref mut pattern,
allow_remote_lookup,
..
Self::Loading {
inner:
KeySelectionLoading {
ref mut progress_spinner,
handles: (ref mut main_handle, ref mut other_handles),
secret,
local,
patterns: (ref mut pattern, ref mut other_patterns),
allow_remote_lookup,
..
},
ref mut keys_accumulator,
} => match event {
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id)) if *id == handle.job_id => {
UIEvent::StatusEvent(StatusEvent::JobFinished(ref id))
if *id == main_handle.job_id
|| other_handles.iter().any(|h| h.job_id == *id) =>
{
let mut main_handle_ref = &mut (*main_handle);
let is_main = *id == main_handle_ref.job_id;
let other_handle_ref_opt = other_handles.iter_mut().find(|h| h.job_id == *id);
let handle = if is_main {
&mut main_handle_ref
} else {
&mut (*other_handle_ref_opt.unwrap())
};
match handle.chan.try_recv() {
Err(_) => { /* Job was canceled */ }
Ok(None) => { /* something happened, perhaps a worker thread panicked */ }
@ -121,20 +196,24 @@ impl Component for KeySelection {
if keys.is_empty() {
let id = progress_spinner.id();
if allow_remote_lookup.is_true() {
match Self::new(
match KeySelectionLoading::new(
*secret,
*local,
std::mem::take(pattern),
(std::mem::take(pattern), std::mem::take(other_patterns)),
*allow_remote_lookup,
context,
) {
Ok(w) => {
*self = w;
Ok(inner) => {
let keys_accumulator = std::mem::take(keys_accumulator);
*self = Self::Loading {
inner,
keys_accumulator,
};
}
Err(err) => *self = KeySelection::Error { err, id },
Err(err) => *self = Self::Error { err, id },
}
} else if !*local && allow_remote_lookup.is_ask() {
*self = KeySelection::Error {
*self = Self::Error {
err: Error::new(format!(
"No keys found for {}, perform remote lookup?",
pattern
@ -142,52 +221,88 @@ impl Component for KeySelection {
id,
}
} else {
*self = KeySelection::Error {
err: Error::new(format!("No keys found for {}.", pattern)),
*self = Self::Error {
err: if pattern.is_empty() {
Error::new("No keys found.")
} else {
Error::new(format!("No keys found for {}.", pattern))
},
id,
}
}
if let KeySelection::Error { ref err, .. } = self {
if let Self::Error { ref err, .. } = self {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
let res: Option<melib::gpgme::Key> = None;
// Even in case of error, we should send a FinishedUIDialog
// event so that the component parent knows we're done.
let res: Option<Vec<melib::gpgme::Key>> = None;
context
.replies
.push_back(UIEvent::FinishedUIDialog(id, Box::new(res)));
}
return false;
}
let mut widget = Box::new(UIDialog::new(
"select key",
keys.iter()
.map(|k| {
(
k.clone(),
if let Some(primary_uid) = k.primary_uid() {
format!("{} {}", k.fingerprint(), primary_uid)
} else {
k.fingerprint().to_string()
},
)
})
.collect::<Vec<(melib::gpgme::Key, String)>>(),
true,
Some(Box::new(
move |id: ComponentId, results: &[melib::gpgme::Key]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(results.get(0).cloned()),
))
},
)),
context,
));
widget.set_dirty(true);
*self = KeySelection::Loaded { widget, keys };
keys_accumulator.extend(keys);
if !is_main {
other_handles.retain(|h| h.job_id != *id);
return false;
}
if other_handles.is_empty() {
// We are done with all Futures, so finally transition into the
// "show the user the list of keys to select" state.
let mut widget = Box::new(UIDialog::new(
"select key",
keys_accumulator
.iter()
.map(|k| {
(
k.clone(),
if let Some(primary_uid) = k.primary_uid() {
format!("{} {}", k.fingerprint(), primary_uid)
} else {
k.fingerprint().to_string()
},
)
})
.collect::<Vec<(melib::gpgme::Key, String)>>(),
false,
Some(Box::new(
move |id: ComponentId, results: &[melib::gpgme::Key]| {
Some(UIEvent::FinishedUIDialog(
id,
Box::new(if results.is_empty() {
None
} else {
Some(results.to_vec())
}),
))
},
)),
context,
));
widget.set_dirty(true);
*self = Self::Loaded {
widget,
keys: std::mem::take(keys_accumulator),
};
} else {
// Main handle has finished, replace it with some other one from
// other_handles.
*main_handle = other_handles.remove(0);
}
}
Ok(Some(Err(err))) => {
*self = KeySelection::Error {
context.replies.push_back(UIEvent::StatusEvent(
StatusEvent::DisplayMessage(err.to_string()),
));
// Even in case of error, we should send a FinishedUIDialog
// event so that the component parent knows we're done.
let res: Option<Vec<melib::gpgme::Key>> = None;
context
.replies
.push_back(UIEvent::FinishedUIDialog(self.id(), Box::new(res)));
*self = Self::Error {
err,
id: ComponentId::default(),
};
@ -197,30 +312,38 @@ impl Component for KeySelection {
}
_ => progress_spinner.process_event(event, context),
},
KeySelection::Error { .. } => false,
KeySelection::Loaded { ref mut widget, .. } => widget.process_event(event, context),
Self::Error { .. } => false,
Self::Loaded { ref mut widget, .. } => widget.process_event(event, context),
}
}
fn is_dirty(&self) -> bool {
match self {
KeySelection::LoadingKeys {
ref progress_spinner,
..
Self::Loading {
inner:
KeySelectionLoading {
ref progress_spinner,
..
},
keys_accumulator: _,
} => progress_spinner.is_dirty(),
KeySelection::Error { .. } => true,
KeySelection::Loaded { ref widget, .. } => widget.is_dirty(),
Self::Error { .. } => true,
Self::Loaded { ref widget, .. } => widget.is_dirty(),
}
}
fn set_dirty(&mut self, value: bool) {
match self {
KeySelection::LoadingKeys {
ref mut progress_spinner,
..
Self::Loading {
inner:
KeySelectionLoading {
ref mut progress_spinner,
..
},
keys_accumulator: _,
} => progress_spinner.set_dirty(value),
KeySelection::Error { .. } => {}
KeySelection::Loaded { ref mut widget, .. } => widget.set_dirty(value),
Self::Error { .. } => {}
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
}
}
@ -228,29 +351,31 @@ impl Component for KeySelection {
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
match self {
KeySelection::LoadingKeys { .. } | KeySelection::Error { .. } => {
ShortcutMaps::default()
}
KeySelection::Loaded { ref widget, .. } => widget.shortcuts(context),
Self::Loading { .. } | Self::Error { .. } => ShortcutMaps::default(),
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
}
}
fn id(&self) -> ComponentId {
match self {
KeySelection::LoadingKeys {
ref progress_spinner,
..
Self::Loading {
inner:
KeySelectionLoading {
ref progress_spinner,
..
},
keys_accumulator: _,
} => progress_spinner.id(),
KeySelection::Error { ref id, .. } => *id,
KeySelection::Loaded { ref widget, .. } => widget.id(),
Self::Error { ref id, .. } => *id,
Self::Loaded { ref widget, .. } => widget.id(),
}
}
}
#[derive(Debug, Clone)]
#[derive(Clone, Debug)]
pub struct GpgComposeState {
pub sign_mail: ToggleFlag,
pub encrypt_mail: ToggleFlag,
pub sign_mail: Option<ActionFlag>,
pub encrypt_mail: Option<ActionFlag>,
pub encrypt_keys: Vec<melib::gpgme::Key>,
pub encrypt_for_self: bool,
pub sign_keys: Vec<melib::gpgme::Key>,
@ -258,12 +383,192 @@ pub struct GpgComposeState {
impl Default for GpgComposeState {
fn default() -> Self {
GpgComposeState {
sign_mail: ToggleFlag::Unset,
encrypt_mail: ToggleFlag::Unset,
Self {
sign_mail: None,
encrypt_mail: None,
encrypt_keys: vec![],
encrypt_for_self: true,
sign_keys: vec![],
}
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, ffi::CString, thread::sleep, time::Duration};
use melib::gpgme::{EngineInfo, LocateKey, Protocol};
use rusty_fork::rusty_fork_test;
use super::*;
impl KeySelection {
fn new_mock(
secret: bool,
local: bool,
pattern: String,
allow_remote_lookup: ActionFlag,
context: &Context,
ctx: &mut melib::gpgme::Context,
) -> Result<Self> {
if local {
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
} else {
ctx.set_auto_key_locate(LocateKey::WKD | LocateKey::LOCAL)?;
}
let job = ctx.keylist(secret, Some(pattern.clone()))?;
let handle = context.main_loop_handler.job_executor.spawn(
"gpg::keylist".into(),
job,
IsAsync::Blocking,
);
let mut progress_spinner = ProgressSpinner::new(8, context);
progress_spinner.start();
Ok(Self::Loading {
inner: KeySelectionLoading {
handles: (handle, vec![]),
secret,
local,
patterns: (pattern, vec![]),
allow_remote_lookup,
progress_spinner,
},
keys_accumulator: vec![],
})
}
}
const PUBKEY: &[u8]=b"-----BEGIN PGP PUBLIC KEY BLOCK-----\r\nVersion: GnuPG v2.1.0-gitb3c71eb (GNU/Linux)\r\n\r\nmQGiBDo41NoRBADSfQazKGYf8nokq6zUKH/6INtV6MypSzSGmX2XErnARkIIPPYj\r\ncQRQ8zCbGV7ZU2ezVbzhFLUSJveE8PZUzzCrLp1O2NSyBTRcR5HVSXW95nJfY8eV\r\npOvZRAKul0BVLh81kYTsrfzaaCjh9VWNP26LoeN2r+PjZyktXe7gM3C4SwCgoTxK\r\nWUVi9HoT2HCLY7p7oig5hEcEALdCJal0UYomX3nJapIVLVZg3vkidr1RICYMb2vz\r\n58i17h8sxEtobD1vdIKNejulntaRAXs4n0tDYD9z7pRlwG1CLz1R9WxYzeOOqUDr\r\nfnVXdmU8L/oVWABat8v1V7QQhjMMf+41fuzVwDMMGqjVPLhu4X6wp3A8uyM3YDnQ\r\nVMN1A/4n2G5gHoOvjqxn8Ch5tBAdMGfO8gH4RjQOwzm2R1wPQss/yzUN1+tlMZGX\r\nK2dQ2FCWC/hDUSNaEQRlI15wxxBNZ2RQwlzE2A8v113DpvyzOtv0QO95gJ1teCXC\r\n7j/BN9asgHaBBc39JLO/TcpuI7Hf8PQ5VcP2F0UE3lczGhXbLLRESm9lIFJhbmRv\r\nbSBIYWNrZXIgKHRlc3Qga2V5IHdpdGggcGFzc3BocmFzZSAiYWJjIikgPGpvZUBl\r\neGFtcGxlLmNvbT6IYgQTEQIAIgUCTbdXqQIbIwYLCQgHAwIGFQgCCQoLBBYCAwEC\r\nHgECF4AACgkQr4IkT5zZ/VUcCACfQvSPi//9/gBv8SVrK6O4DiyD+jAAn3LEnfF1\r\n4j6MjwlqXTqol2VgQn1yuQENBDo41N0QBACedJb7Qhm50JSPe1V+rSZKLHT5nc3l\r\n2k1n7//wNsJkgDW2J7snIRjGtSzeNxMPh+hVzFidzAf3sbOlARQoBrMPPKpnJWtm\r\n6LEDf2lSwO36l0/bo6qDRmiFRJoHWytTJEjxVwRclVt4bXqHfNw9FKhZZbcKeAN2\r\noHgmBVSU6edHdwADBQP+OGAkEG4PcfSb8x191R+wkV/q2hA5Ay9z289Dx2rO28CO\r\n4M2fhhcjSmgr6x0DsrkfESCiG47UGJ169eu+QqJwk3HiF4crGN9rE5+VelBVFtrd\r\nMWkX2rPLGQWyw8iCZKbeH8g/ujmkaLovSmalzDcLe4v1xSLaP7Fnfzit0iIGZAGI\r\nRgQYEQIABgUCOjjU3QAKCRCvgiRPnNn9VVSaAJ9+rj1lIQnRl20i8Rom2Hwbe3re\r\n9QCfSYFnkZUw0yKF2DfCfqrDzdGAsbaIRgQYEQIABgUCOjjU3gAKCRCvgiRPnNn9\r\nVe4iAJ9FrGMlFR7s+GWf1scTeeyrthKrPQCfSpc/Yps72aFI7hPfyIa9MuerVZ4=\r\n=QRit\r\n-----END PGP PUBLIC KEY BLOCK-----\r\n";
rusty_fork_test! {
#[test]
fn test_gpg_verify_sig() {
let tempdir = tempfile::tempdir().unwrap();
{
#[allow(unused_unsafe)]
unsafe {
std::env::set_var("GNUPGHOME", tempdir.path());
}
#[allow(unused_unsafe)]
unsafe {
std::env::set_var("GPG_AGENT_INFO", "");
}
}
let mut ctx = Context::new_mock(&tempdir);
let mut gpgme_ctx = match melib::gpgme::Context::new() {
Ok(v) => v,
Err(err) if err.kind.is_not_found() => {
eprintln!("INFO: libgpgme could not be loaded, skipping this test.");
return;
}
err => err.unwrap(),
};
let current_engine_info = gpgme_ctx.engine_info().unwrap();
let prev_len = current_engine_info.len();
let Some(EngineInfo {
file_name: Some(engine_file_name),
..
}) = current_engine_info
.into_iter()
.find(|eng| eng.protocol == Protocol::OpenPGP)
else {
eprintln!("WARN: No openpg protocol engine returned from gpgme.");
return;
};
gpgme_ctx
.set_engine_info(
Protocol::OpenPGP,
Some(Cow::Owned(CString::new(engine_file_name).unwrap())),
Some(Cow::Owned(
CString::new(tempdir.path().display().to_string()).unwrap(),
)),
)
.unwrap();
let new_engine_info = gpgme_ctx.engine_info().unwrap();
assert_eq!(
new_engine_info.len(),
prev_len,
"new_engine_info was expected to have {} entry/ies but has {}: {:#?}",
prev_len,
new_engine_info.len(),
new_engine_info
);
assert_eq!(
new_engine_info[0].home_dir,
Some(tempdir.path().display().to_string()),
"new_engine_info was expected to have temp dir as home_dir but has: {:#?}",
new_engine_info[0].home_dir
);
let mut pubkey_data = Some(gpgme_ctx.new_data_mem(PUBKEY).unwrap());
for _ in 0..2 {
let mut key_sel = KeySelection::new_mock(
false,
true,
"".to_string(),
false.into(),
&ctx,
&mut gpgme_ctx,
)
.unwrap();
let component_id = key_sel.id();
for _ in 0..2 {
sleep(Duration::from_secs(2));
}
while let Ok(ev) = ctx.receiver.try_recv() {
// if !matches!(ev, ThreadEvent::UIEvent(UIEvent::Timer(_))) {
// dbg!(&ev);
// }
if let ThreadEvent::UIEvent(mut ev) = ev {
key_sel.process_event(&mut ev, &mut ctx);
} else if let ThreadEvent::JobFinished(job_id) = ev {
let mut ev = UIEvent::StatusEvent(StatusEvent::JobFinished(job_id));
key_sel.process_event(&mut ev, &mut ctx);
}
}
if let Some(pubkey_data) = pubkey_data.take() {
assert!(
matches!(
key_sel,
KeySelection::Error {
ref id,
ref err
} if *id == component_id && err.to_string() == melib::Error::new("No keys found.").to_string(),
),
"key_sel should have been an error but is: {:?}",
key_sel
);
gpgme_ctx.import_key(pubkey_data).unwrap();
} else {
let assert_key = |key: &melib::gpgme::Key| {
key.fingerprint() == "ADAB7FCC1F4DE2616ECFA402AF82244F9CD9FD55"
&& key.primary_uid()
== Some(melib::Address::new(
Some("Joe Random Hacker".into()),
"joe@example.com".into(),
))
&& key.can_encrypt()
&& key.can_sign()
&& !key.secret()
&& !key.revoked()
&& !key.expired()
&& !key.invalid()
};
assert!(
matches!(
key_sel,
KeySelection::Loaded {
ref keys,
widget: _,
} if keys.len() == 1 && assert_key(&keys[0]),
),
"key_sel should have been an error but is: {:?}",
key_sel
);
}
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more