Compare commits
No commits in common. "master" and "fix/242" have entirely different histories.
|
@ -1,2 +0,0 @@
|
|||
# Use cargo-derivefmt to sort derives alphabetically
|
||||
f900dbea468e822c5a510a72ecc6367549443927
|
|
@ -1,37 +0,0 @@
|
|||
# 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
|
|
@ -1,61 +0,0 @@
|
|||
# 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)
|
|
@ -1,28 +0,0 @@
|
|||
# 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
|
|
@ -1,41 +0,0 @@
|
|||
---
|
||||
|
||||
name: "Pull Request"
|
||||
about: "Basic pull request template"
|
||||
ref: "master"
|
||||
|
||||
---
|
||||
|
||||
<!-- 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.
|
||||
|
||||
|
||||
## 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.
|
||||
|
||||
-->
|
|
@ -1,109 +0,0 @@
|
|||
#!/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
|
|
@ -1,123 +0,0 @@
|
|||
# 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
|
|
@ -1,79 +0,0 @@
|
|||
# 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
|
|
@ -1,71 +0,0 @@
|
|||
# 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
|
|
@ -1,26 +0,0 @@
|
|||
# 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
|
|
@ -1,140 +0,0 @@
|
|||
# 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
|
|
@ -1,98 +0,0 @@
|
|||
# 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
|
8
.mailmap
|
@ -1,8 +0,0 @@
|
|||
# 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
|
@ -1,91 +0,0 @@
|
|||
# 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`
|
1321
CHANGELOG.md
1985
Cargo.lock
generated
95
Cargo.toml
|
@ -1,10 +1,74 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
[package]
|
||||
name = "meli"
|
||||
version = "0.7.2"
|
||||
authors = ["Manos Pitsidianakis <el13635@mail.ntua.gr>"]
|
||||
edition = "2018"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
members = [
|
||||
"meli",
|
||||
"melib",
|
||||
]
|
||||
license = "GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
description = "terminal mail client"
|
||||
homepage = "https://meli.delivery"
|
||||
repository = "https://git.meli.delivery/meli/meli.git"
|
||||
keywords = ["mail", "mua", "maildir", "terminal", "imap"]
|
||||
categories = ["command-line-utilities", "email"]
|
||||
default-run = "meli"
|
||||
|
||||
[[bin]]
|
||||
name = "meli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "managesieve-client"
|
||||
path = "src/managesieve.rs"
|
||||
required-features = ["melib/imap_backend"]
|
||||
|
||||
[dependencies]
|
||||
async-task = "^4.2.0"
|
||||
bitflags = "1.0"
|
||||
crossbeam = { version = "^0.8" }
|
||||
flate2 = { version = "1", optional = true }
|
||||
futures = "0.3.5"
|
||||
indexmap = { version = "^1.6", features = ["serde-1", ] }
|
||||
libc = { version = "0.2.125", default-features = false, features = ["extra_traits",] }
|
||||
linkify = { version = "^0.8", default-features = false }
|
||||
melib = { path = "melib", version = "0.7.2" }
|
||||
nix = { version = "^0.24", default-features = false }
|
||||
notify = { version = "4.0.1", default-features = false } # >:c
|
||||
num_cpus = "1.12.0"
|
||||
pcre2 = { version = "0.2.3", optional = true }
|
||||
|
||||
serde = "1.0.71"
|
||||
serde_derive = "1.0.71"
|
||||
serde_json = "1.0"
|
||||
signal-hook = { version = "^0.3", default-features = false }
|
||||
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" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
toml = { version = "0.5.6", default-features = false, features = ["preserve_order", ] }
|
||||
unicode-segmentation = "1.2.1" # >:c
|
||||
xdg = "2.1.0"
|
||||
|
||||
[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"
|
||||
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
|
@ -12,3 +76,22 @@ codegen-units = 1
|
|||
opt-level = "s"
|
||||
debug = false
|
||||
strip = true
|
||||
|
||||
[workspace]
|
||||
members = ["melib", "tools", ]
|
||||
|
||||
[features]
|
||||
default = ["sqlite3", "notmuch", "regexp", "smtp", "dbus-notifications", "gpgme", "cli-docs"]
|
||||
notmuch = ["melib/notmuch_backend", ]
|
||||
jmap = ["melib/jmap_backend",]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
smtp = ["melib/smtp"]
|
||||
regexp = ["pcre2"]
|
||||
dbus-notifications = ["notify-rust",]
|
||||
cli-docs = ["flate2"]
|
||||
svgscreenshot = ["svg_crate"]
|
||||
gpgme = ["melib/gpgme"]
|
||||
|
||||
# Print tracing logs as meli runs in stderr
|
||||
# enable for debug tracing logs: build with --features=debug-tracing
|
||||
debug-tracing = ["melib/debug-tracing", ]
|
||||
|
|
21
Cross.toml
|
@ -1,21 +0,0 @@
|
|||
[target.aarch64-unknown-linux-gnu]
|
||||
# Build with -static features.
|
||||
pre-build = [
|
||||
"export DEBIAN_FRONTEND=noninteractive ",
|
||||
"dpkg --add-architecture $CROSS_DEB_ARCH",
|
||||
"apt-get update -y",
|
||||
"""
|
||||
apt-get install --assume-yes \
|
||||
pkg-config \
|
||||
libdbus-1-dev \
|
||||
libdbus-1-dev:$CROSS_DEB_ARCH \
|
||||
librust-libdbus-sys-dev \
|
||||
librust-libdbus-sys-dev:$CROSS_DEB_ARCH \
|
||||
librust-openssl-sys-dev \
|
||||
librust-openssl-sys-dev:$CROSS_DEB_ARCH \
|
||||
libsqlite3-dev:$CROSS_DEB_ARCH \
|
||||
libssl-dev \
|
||||
libssl-dev:$CROSS_DEB_ARCH \
|
||||
sqlite3:$CROSS_DEB_ARCH
|
||||
""",
|
||||
]
|
|
@ -1,67 +0,0 @@
|
|||
# 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
|
||||
cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
|
||||
```
|
||||
|
||||
## Profiling
|
||||
|
||||
```sh
|
||||
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 -->
|
||||
<!-- ``` -->
|
187
Makefile
|
@ -19,13 +19,11 @@
|
|||
.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
|
||||
CARGO_HACK_BIN = cargo-hack
|
||||
PRINTF := `command -v printf`
|
||||
PRINTF = /usr/bin/printf
|
||||
|
||||
# Options
|
||||
PREFIX ?= /usr/local
|
||||
|
@ -34,15 +32,14 @@ BINDIR ?= ${EXPANDED_PREFIX}/bin
|
|||
MANDIR ?= ${EXPANDED_PREFIX}/share/man
|
||||
|
||||
# Installation parameters
|
||||
DOCS_SUBDIR ?= meli/docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5 meli.7
|
||||
FEATURES != [ -z "$${MELI_FEATURES}" ] && ($(PRINTF) -- '--all-features') || ($(PRINTF) -- '--features %s' "$${MELI_FEATURES}")
|
||||
DOCS_SUBDIR ?= docs/
|
||||
MANPAGES ?= meli.1 meli.conf.5 meli-themes.5
|
||||
FEATURES ?= --features "${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 $${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`
|
||||
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
|
||||
|
||||
# Output parameters
|
||||
BOLD ?= `[ -z $${TERM} ] && echo "" || tput bold`
|
||||
|
@ -51,21 +48,19 @@ 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\n${BOLD}${GREEN}make PREFIX=~/.local install${ANSI_RESET}\n"
|
||||
@echo "For a quick start, build and install locally:\n ${BOLD}${GREEN}PREFIX=~/.local make 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 "\nSecondary subcommands:"
|
||||
@echo "Secondary 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)"
|
||||
|
@ -76,65 +71,44 @@ 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 ""
|
||||
@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 "\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 "* (cleaned) output of manpath(1) = ${UNDERLINE}${MANPATHS}${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 -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}
|
||||
@echo "* CARGO_BIN = ${UNDERLINE}${CARGO_BIN}${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 "* CARGO_ARGS = ${UNDERLINE}${CARGO_ARGS}${ANSI_RESET}"
|
||||
@echo "* MIN_RUSTC = ${UNDERLINE}${MIN_RUSTC}${ANSI_RESET}"
|
||||
@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}"
|
||||
@#@echo "* CARGO_COLOR = ${CARGO_COLOR}"
|
||||
|
||||
.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
|
||||
check:
|
||||
@${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) 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"
|
||||
@$(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"
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@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
|
||||
@$(CARGO_BIN) clippy --no-deps --all-features --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: test
|
||||
test: test-docs
|
||||
@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:
|
||||
@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
|
||||
test:
|
||||
@${CARGO_BIN} test ${CARGO_ARGS} ${CARGO_COLOR}--target-dir="${CARGO_TARGET_DIR}" --all --tests --examples --benches --bins
|
||||
|
||||
.PHONY: check-deps
|
||||
check-deps:
|
||||
|
@ -147,116 +121,67 @@ clean:
|
|||
-rm -rf ./${CARGO_TARGET_DIR}/
|
||||
|
||||
.PHONY: distclean
|
||||
distclean:
|
||||
rm -f meli-${VERSION}.tar.gz
|
||||
rm -rf .pc # rm debian stuff
|
||||
distclean: clean
|
||||
@rm -f meli-${VERSION}.tar.gz
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
rm -f $(DESTDIR)${BINDIR}/meli
|
||||
for MANPAGE in ${MANPAGES}; do \
|
||||
SECTION=`echo $${MANPAGE} | rev | cut -d "." -f 1`; \
|
||||
MANPAGEPATH="${DESTDIR}${MANDIR}/man$${SECTION}/$${MANPAGE}.gz"; \
|
||||
rm -f "$${MANAGEPATH}"
|
||||
; done
|
||||
-rm $(DESTDIR)${MANDIR}/man1/meli.1.gz
|
||||
-rm $(DESTDIR)${MANDIR}/man5/meli.conf.5.gz
|
||||
-rm $(DESTDIR)${MANDIR}/man5/meli-themes.5.gz
|
||||
|
||||
.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 "\c";; \
|
||||
*:${DESTDIR}${MANDIR}:*) echo -n "";; \
|
||||
*) 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 \
|
||||
$(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" ;\
|
||||
echo "\n You're ready to go. You might want to read the \"STARTING WITH meli\" section in the manpage (\`man meli\`)" ;\
|
||||
fi)
|
||||
@$(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"
|
||||
@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."
|
||||
|
||||
.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:
|
||||
@$(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
|
||||
@dpkg-buildpackage -b -rfakeroot -us -uc
|
||||
@echo ${BOLD}${GREEN}Generated${ANSI_RESET} ../meli_${VERSION}-1_amd64.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
|
||||
check-tagrefs:
|
||||
@(if ! command -v "$(TAGREF_BIN)" > /dev/null;\
|
||||
then \
|
||||
$(PRINTF) "Warning: tagref binary not in PATH.\n" 1>&2;\
|
||||
exit;\
|
||||
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-)
|
||||
#}
|
||||
|
|
251
README.md
|
@ -1,188 +1,137 @@
|
|||
# meli   [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli) [](ircs://irc.oftc.net:6697/%23meli)
|
||||
# meli [](https://github.com/meli/meli/blob/master/COPYING) [](https://crates.io/crates/meli)
|
||||
|
||||
**BSD/Linux/macos terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP / NNTP (Usenet).**
|
||||
**BSD/Linux terminal email client with support for multiple accounts and Maildir / mbox / notmuch / IMAP / JMAP.**
|
||||
|
||||
Try an [old, outdated but online and interactive web demo](https://meli-email.org/wasm2.html "online interactive web demo") powered by WebAssembly!
|
||||
Community links:
|
||||
[mailing lists](https://lists.meli.delivery/) | `#meli` on OFTC IRC | Report bugs and/or feature requests in [meli's issue tracker](https://git.meli.delivery/meli/meli/issues "meli gitea issue tracker")
|
||||
|
||||
* `#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>
|
||||
| | | |
|
||||
:---:|:---:|:---:
|
||||
 |  | 
|
||||
Main view | Compact main view | Compose with embed terminal editor
|
||||
|
||||
**Table of contents**:
|
||||
Main repository:
|
||||
* https://git.meli.delivery/meli/meli
|
||||
|
||||
- [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)
|
||||
Official mirrors:
|
||||
* https://github.com/meli/meli
|
||||
|
||||
## Install
|
||||
- Try an [online interactive web demo](https://meli.delivery/wasm2.html "online interactive web demo") powered by WebAssembly
|
||||
- [`cargo install meli`](https://crates.io/crates/meli "crates.io meli package")
|
||||
- [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>
|
||||
## Documentation
|
||||
|
||||
- `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
|
||||
See a comprehensive tour of `meli` in the manual page [`meli(7)`](./docs/meli.7).
|
||||
|
||||
## Build
|
||||
See also the [Quickstart tutorial](https://meli.delivery/documentation.html#quick-start) online.
|
||||
|
||||
Run `make` or `cargo build --release --bin meli`.
|
||||
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 `docs/samples/` subdirectory. Manual pages are also [hosted online](https://meli.delivery/documentation.html "meli documentation").
|
||||
|
||||
For detailed building instructions, see [`BUILD.md`](./BUILD.md)
|
||||
`meli` by default looks for a configuration file in this location: `$XDG_CONFIG_HOME/meli/config.toml`
|
||||
|
||||
### 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
|
||||
|
||||
```sh
|
||||
# Create configuration file in ${XDG_CONFIG_HOME}/meli/config.toml:
|
||||
$ meli create-config
|
||||
# Edit configuration in ${EDITOR} or ${VISUAL}:
|
||||
$ meli edit-config
|
||||
# Optionally, install manual pages if installed via cargo:
|
||||
$ 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
|
||||
```
|
||||
|
||||
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.
|
||||
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.:
|
||||
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
|
||||
```
|
||||
|
||||
See [`meli(7)`](./meli/docs/meli.7) for an extensive tutorial and [`meli.conf(5)`](./meli/docs/meli.conf.5) for all configuration values.
|
||||
## Build
|
||||
For a quick start, build and install locally:
|
||||
|
||||
| Main view | Compact main view | Compose with embed terminal editor |
|
||||
|-----------|-------------------|------------------------------------|
|
||||
|  |  |  |
|
||||
```sh
|
||||
PREFIX=~/.local make install
|
||||
```
|
||||
|
||||
### Supported E-mail backends
|
||||
Available subcommands for `make` are listed with `make help`. The Makefile *should* be POSIX portable and not require a specific `make` version.
|
||||
|
||||
| Protocol | Support |
|
||||
|---------------|------------|
|
||||
| IMAP | full |
|
||||
| Maildir | full |
|
||||
| notmuch | full[^0] |
|
||||
| mbox | read-only |
|
||||
| JMAP | functional |
|
||||
| NNTP / Usenet | functional |
|
||||
`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>
|
||||
|
||||
[^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
|
||||
inside that mailbox.
|
||||
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`.
|
||||
|
||||
### E-mail submission backends
|
||||
You can build and run `meli` with one command: `cargo run --release`.
|
||||
|
||||
- SMTP
|
||||
- Pipe to shell script
|
||||
- Server-side submission when supported
|
||||
### Build features
|
||||
|
||||
### Non-exhaustive list of features
|
||||
Some functionality is held behind "feature gates", or compile-time flags. The following list explains each feature's purpose:
|
||||
|
||||
- TLS
|
||||
- email threading support
|
||||
- multithreaded, async operation
|
||||
- optionally run your editor of choice inside meli, with an embedded
|
||||
xterm-compatible terminal emulator
|
||||
- plain text configuration in TOML
|
||||
- ability to open emails in UI tabs and switch to them
|
||||
- optional sqlite3 index search
|
||||
- override almost any setting per mailbox, per account
|
||||
- contact list (+read-only vCard and mutt alias file support)
|
||||
- forced UTF-8 (other encodings are read-only)
|
||||
- configurable shortcuts
|
||||
- theming
|
||||
- `NO_COLOR` support
|
||||
- ascii-only drawing characters option
|
||||
- view text/html attachments through an html filter command (w3m by default)
|
||||
- pipe attachments/mail to stuff
|
||||
- use external attachment file picker instead of typing in an attachment's full path
|
||||
- GPG signing, encryption, signing + encryption
|
||||
- GPG signature verification
|
||||
- `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 (off 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]`
|
||||
- `svgscreenshot` provides support for taking screenshots of the current view of `meli` and saving it as SVG files. Its only purpose is taking screenshots for the official `meli` webpage. (off by default)
|
||||
- `debug-tracing` enables various trace debug logs from various places around the `meli` code base. The trace log is printed in `stderr`. (off by default)
|
||||
|
||||
## HTML Rendering
|
||||
### 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.
|
||||
|
||||
### Building with JMAP
|
||||
|
||||
To build with JMAP support, prepend the environment variable `MELI_FEATURES='jmap'` to your make invocation:
|
||||
|
||||
```sh
|
||||
MELI_FEATURES="jmap" make
|
||||
```
|
||||
|
||||
or if building directly with cargo, use the flag `--features="jmap"'.
|
||||
|
||||
### 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)).
|
||||
You can use the `pager.html_filter` setting to override this (for more details you can consult [`meli.conf(5)`](./docs/meli.conf.5)).
|
||||
|
||||
# Development
|
||||
|
||||
## Documentation
|
||||
Development builds can be built and/or run with
|
||||
|
||||
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
|
||||
```
|
||||
cargo build
|
||||
cargo run
|
||||
```
|
||||
|
||||
or
|
||||
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, thus you can run `meli` with a redirection (i.e `2> log`)
|
||||
|
||||
Code style follows the default rustfmt profile.
|
||||
|
||||
## Testing
|
||||
|
||||
How to run specific tests:
|
||||
|
||||
```sh
|
||||
meli -c ./test_config
|
||||
cargo test -p {melib, meli} (-- --nocapture) (--test test_name)
|
||||
```
|
||||
|
||||
## Profiling
|
||||
|
||||
```sh
|
||||
perf record -g target/debug/bin
|
||||
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
|
||||
```
|
||||
|
|
|
@ -22,11 +22,12 @@
|
|||
extern crate proc_macro;
|
||||
extern crate quote;
|
||||
extern crate syn;
|
||||
include!("config_macros.rs");
|
||||
mod config_macros;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=build.rs");
|
||||
println!("cargo:rerun-if-changed=src/conf/.rebuild.overrides.rs");
|
||||
override_derive(&[
|
||||
config_macros::override_derive(&[
|
||||
("src/conf/pager.rs", "PagerSettings"),
|
||||
("src/conf/listing.rs", "ListingSettings"),
|
||||
("src/conf/notifications.rs", "NotificationsSettings"),
|
||||
|
@ -39,57 +40,37 @@ fn main() {
|
|||
{
|
||||
use flate2::{Compression, GzBuilder};
|
||||
const MANDOC_OPTS: &[&str] = &["-T", "utf8", "-I", "os=Generated by mandoc(1)"];
|
||||
use std::{env, io::prelude::*, path::Path};
|
||||
use std::{env, fs::File, io::prelude::*, path::Path, process::Command};
|
||||
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let mut out_dir_path = Path::new(&out_dir).to_path_buf();
|
||||
|
||||
let mut cl = |filepath: &str, output: &str, source: bool| {
|
||||
let mut cl = |filepath: &str, output: &str| {
|
||||
out_dir_path.push(output);
|
||||
let output = if source {
|
||||
std::fs::read_to_string(filepath).unwrap().into_bytes()
|
||||
} else {
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg(filepath)
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
|
||||
.expect(
|
||||
"could not execute `mandoc` or `man`. If the binaries are not available \
|
||||
in the PATH, disable `cli-docs` feature to be able to continue \
|
||||
compilation.",
|
||||
);
|
||||
output.stdout
|
||||
};
|
||||
let output = Command::new("mandoc")
|
||||
.args(MANDOC_OPTS)
|
||||
.arg(filepath)
|
||||
.output()
|
||||
.or_else(|_| Command::new("man").arg("-l").arg(filepath).output())
|
||||
.expect(
|
||||
"could not execute `mandoc` or `man`. If the binaries are not available in \
|
||||
the PATH, disable `cli-docs` feature to be able to continue compilation.",
|
||||
);
|
||||
|
||||
let file = File::create(&out_dir_path).unwrap_or_else(|err| {
|
||||
panic!("Could not create file {}: {}", out_dir_path.display(), err)
|
||||
});
|
||||
let mut gz = GzBuilder::new()
|
||||
.comment(output.len().to_string().into_bytes())
|
||||
.comment(output.stdout.len().to_string().into_bytes())
|
||||
.write(file, Compression::default());
|
||||
gz.write_all(&output).unwrap();
|
||||
gz.write_all(&output.stdout).unwrap();
|
||||
gz.finish().unwrap();
|
||||
out_dir_path.pop();
|
||||
};
|
||||
|
||||
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);
|
||||
cl("docs/meli.1", "meli.txt.gz");
|
||||
cl("docs/meli.conf.5", "meli.conf.txt.gz");
|
||||
cl("docs/meli-themes.5", "meli-themes.txt.gz");
|
||||
cl("docs/meli.7", "meli.7.txt.gz");
|
||||
}
|
||||
}
|
166
cliff.toml
|
@ -1,166 +0,0 @@
|
|||
# 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"
|
|
@ -1,69 +0,0 @@
|
|||
{
|
||||
"@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"
|
||||
}
|
|
@ -29,7 +29,7 @@ use quote::{format_ident, quote};
|
|||
use regex::Regex;
|
||||
|
||||
// Write ConfigStructOverride to overrides.rs
|
||||
pub(crate) fn override_derive(filenames: &[(&str, &str)]) {
|
||||
pub fn override_derive(filenames: &[(&str, &str)]) {
|
||||
let mut output_file =
|
||||
File::create("src/conf/overrides.rs").expect("Unable to open output file");
|
||||
let mut output_string = r##"// @generated
|
||||
|
@ -56,21 +56,16 @@ 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 {
|
||||
|
@ -125,14 +120,6 @@ use crate::conf::{*, data_types::*};
|
|||
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, "")
|
||||
|
@ -167,11 +154,6 @@ use crate::conf::{*, data_types::*};
|
|||
} 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() {
|
||||
|
@ -210,7 +192,7 @@ use crate::conf::{*, data_types::*};
|
|||
#(#attrs_tokens)*
|
||||
impl Default for #override_ident {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
#override_ident {
|
||||
#(#field_idents: None),*
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<!-- 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`.
|
|
@ -1,21 +0,0 @@
|
|||
#!/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 "$@"
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
#!/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
|
875
debian/changelog
vendored
|
@ -1,876 +1,3 @@
|
|||
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
|
||||
|
||||
|
@ -898,7 +25,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 unused conf flags in some imap/nntp
|
||||
- configs: throw error on extra unusued conf flags in some imap/nntp
|
||||
- configs: throw error on missing `composing` section with explanation
|
||||
|
||||
Fixed
|
||||
|
|
1
debian/compat
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
11
|
18
debian/control
vendored
|
@ -1,20 +1,14 @@
|
|||
Source: meli
|
||||
Section: mail
|
||||
Priority: optional
|
||||
Maintainer: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Build-Depends: debhelper-compat (=13), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Maintainer: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Build-Depends: debhelper (>=11~), mandoc (>=1.14.4-1), quilt, libsqlite3-dev
|
||||
Standards-Version: 4.1.4
|
||||
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
|
||||
Homepage: https://meli.delivery
|
||||
|
||||
Package: meli
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends: ${misc:Depends}, ${shlibs:Depends}
|
||||
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.
|
||||
Recommends: libnotmuch, xdg-utils (>=1.1.3-1)
|
||||
Description: terminal mail client
|
||||
|
|
4
debian/copyright
vendored
|
@ -1,11 +1,11 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: meli
|
||||
Source: https://git.meli-email.org/meli/meli
|
||||
Source: <https://git.meli.delivery/meli/meli>
|
||||
#
|
||||
# Please double check copyright with the licensecheck(1) command.
|
||||
|
||||
Files: *
|
||||
Copyright: 2017-2023 Manos Pitsidianakis
|
||||
Copyright: 2017-2020 Manos Pitsidianakis
|
||||
License: GPL-3.0+
|
||||
#----------------------------------------------------------------------------
|
||||
# License file: COPYING
|
||||
|
|
8
debian/extra/meli.desktop
vendored
|
@ -1,8 +0,0 @@
|
|||
[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
|
@ -1,7 +0,0 @@
|
|||
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
|
@ -1,9 +0,0 @@
|
|||
#!/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
|
@ -1,5 +0,0 @@
|
|||
Document: meli
|
||||
Title: meli E-mail Client Manual
|
||||
Author: Various
|
||||
Abstract: Manual for meli the terminal e-mail client.
|
||||
Section: Network/Communication
|
7
debian/meli.docs
vendored
|
@ -1,4 +1,3 @@
|
|||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
||||
meli/docs/external-tools.md
|
||||
docs/meli.1
|
||||
docs/meli.conf.5
|
||||
docs/meli-themes.5
|
||||
|
|
3
debian/meli.examples
vendored
|
@ -1,3 +0,0 @@
|
|||
contrib/*
|
||||
meli/docs/mail.vim
|
||||
meli/docs/samples/*
|
3
debian/meli.manpages
vendored
|
@ -1,3 +0,0 @@
|
|||
meli/docs/*.1
|
||||
meli/docs/*.5
|
||||
meli/docs/*.7
|
4
debian/patches/fix-prefix-for-debian.patch
vendored
|
@ -1,6 +1,6 @@
|
|||
Description: Fix PREFIX env var in Makefile for use in Debian
|
||||
Author: Manos Pitsidianakis <manos@pitsidianak.is>
|
||||
Last-Update: 2024-11-19
|
||||
Author: Manos Pitsidianakis <epilys@nessuent.xyz>
|
||||
Last-Update: 2023-03-06
|
||||
Index: meli/Makefile
|
||||
===================================================================
|
||||
--- meli.orig/Makefile
|
||||
|
|
1
debian/patches/series
vendored
|
@ -1,2 +1 @@
|
|||
fix-prefix-for-debian.patch
|
||||
usr_bin_editor.patch
|
||||
|
|
18
debian/patches/usr_bin_editor.patch
vendored
|
@ -1,18 +0,0 @@
|
|||
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
|
@ -1,22 +1,14 @@
|
|||
#!/usr/bin/make -f
|
||||
# You must remove unused comment lines for the released package.
|
||||
export RUSTUP_HOME=${HOME}/.rustup
|
||||
export DH_VERBOSE = 1
|
||||
export NO_MAN
|
||||
#export DH_VERBOSE = 1
|
||||
#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
|
@ -1,191 +0,0 @@
|
|||
# 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 = [""]
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Sending mail with a command line tool
|
||||
|
||||
`send_mail` can use either settings for an SMTP server or a shell
|
||||
`composing.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,6 +12,7 @@ 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'
|
||||
```
|
||||
|
@ -119,32 +120,3 @@ 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/
|
|
@ -17,8 +17,7 @@
|
|||
.\" 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 March 10, 2024
|
||||
.Dd November 11, 2022
|
||||
.Dt MELI-THEMES 5
|
||||
.Os
|
||||
.Sh NAME
|
||||
|
@ -32,15 +31,15 @@ comes with two themes,
|
|||
.Ic dark
|
||||
(default) and
|
||||
.Ic light .
|
||||
.Pp
|
||||
.sp
|
||||
Custom themes are defined as lists of key-values in the configuration files:
|
||||
.Bl -item -compact -offset 2
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It
|
||||
.Pa $XDG_CONFIG_HOME/meli/themes/*.toml
|
||||
.El
|
||||
.Pp
|
||||
.sp
|
||||
The application theme is defined in the configuration as follows:
|
||||
.Bd -literal
|
||||
[terminal]
|
||||
|
@ -57,9 +56,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.
|
||||
.Pp
|
||||
.sp
|
||||
.Dl \&"widget.key.label\&" = { fg = \&"Default\&", bg = \&"Default\&", attrs = \&"Default\&" }
|
||||
.Pp
|
||||
.sp
|
||||
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
|
||||
|
@ -70,14 +69,10 @@ 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:
|
||||
.Pp
|
||||
.sp
|
||||
.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,
|
||||
.Ql light
|
||||
and
|
||||
.Ql dark Ns
|
||||
\&.
|
||||
Two themes are included by default, `light` and `dark`.
|
||||
.Sh EXAMPLES
|
||||
Specific settings from already defined themes can be overwritten:
|
||||
.Bd -literal
|
||||
|
@ -105,18 +100,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:
|
||||
.Pp
|
||||
.sp
|
||||
.Dl meli print-default-theme > ~/.config/meli/themes/new_theme.toml
|
||||
.Pp
|
||||
.sp
|
||||
.Pa new_theme.toml
|
||||
will now include all keys and values of the "dark" theme.
|
||||
.Pp
|
||||
.sp
|
||||
.Dl meli print-loaded-themes
|
||||
.Pp
|
||||
.sp
|
||||
will print all loaded themes with the links resolved.
|
||||
.Sh VALID ATTRIBUTE VALUES
|
||||
Case-sensitive.
|
||||
.Bl -dash -compact
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
"Default"
|
||||
.It
|
||||
|
@ -128,8 +123,6 @@ Case-sensitive.
|
|||
.It
|
||||
"Underline"
|
||||
.It
|
||||
"Undercurl"
|
||||
.It
|
||||
"Blink"
|
||||
.It
|
||||
"Reverse"
|
||||
|
@ -140,7 +133,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 -dash -compact
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
"Default" is the terminal default. (Case-sensitive)
|
||||
.It
|
||||
|
@ -153,10 +146,8 @@ 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
|
||||
.Em ANSI
|
||||
colors, there are two options:
|
||||
.Bl -dash -compact
|
||||
To completely disable ANSI colors, there are two options:
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
Set the
|
||||
.Ic use_color
|
||||
|
@ -166,22 +157,17 @@ option (section
|
|||
.It
|
||||
The
|
||||
.Ev NO_COLOR
|
||||
environmental variable, when present (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
color.
|
||||
environmental variable, when present (regardless of its value), prevents the addition of ANSI color.
|
||||
When the configuration value
|
||||
.Ic use_color
|
||||
is explicitly set to true by the user,
|
||||
.Ev NO_COLOR
|
||||
is ignored.
|
||||
.El
|
||||
.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.
|
||||
.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.
|
||||
.Sh VALID KEYS
|
||||
.Bl -dash -compact
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
theme_default
|
||||
.It
|
||||
|
@ -251,10 +237,6 @@ mail.listing.compact.even_highlighted
|
|||
.It
|
||||
mail.listing.compact.odd_highlighted
|
||||
.It
|
||||
mail.listing.compact.even_highlighted_selected
|
||||
.It
|
||||
mail.listing.compact.odd_highlighted_selected
|
||||
.It
|
||||
mail.listing.plain.even
|
||||
.It
|
||||
mail.listing.plain.odd
|
||||
|
@ -271,10 +253,6 @@ mail.listing.plain.even_highlighted
|
|||
.It
|
||||
mail.listing.plain.odd_highlighted
|
||||
.It
|
||||
mail.listing.plain.even_highlighted_selected
|
||||
.It
|
||||
mail.listing.plain.odd_highlighted_selected
|
||||
.It
|
||||
mail.listing.conversations
|
||||
.It
|
||||
mail.listing.conversations.subject
|
||||
|
@ -289,8 +267,6 @@ mail.listing.conversations.highlighted
|
|||
.It
|
||||
mail.listing.conversations.selected
|
||||
.It
|
||||
mail.listing.conversations.highlighted_selected
|
||||
.It
|
||||
mail.view.headers
|
||||
.It
|
||||
mail.view.headers_names
|
||||
|
@ -326,7 +302,7 @@ pager.highlight_search_current
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
Aqua:14:_:Black:0
|
||||
Aquamarine1:122:_:Maroon:1
|
||||
Aquamarine2:86:_:Green:2
|
||||
|
@ -362,7 +338,7 @@ DarkMagenta1:91:_:SpringGreen6:29
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
DarkOliveGreen1:192:_:Turquoise4:30
|
||||
DarkOliveGreen2:155:_:DeepSkyBlue3:31
|
||||
DarkOliveGreen3:191:_:DeepSkyBlue4:32
|
||||
|
@ -398,7 +374,7 @@ DeepPink4:125:_:Grey37:59
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
DeepPink6:162:_:MediumPurple6:60
|
||||
DeepPink7:89:_:SlateBlue2:61
|
||||
DeepPink8:53:_:SlateBlue3:62
|
||||
|
@ -434,7 +410,7 @@ Grey19:236:_:DeepPink7:89
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
Grey23:237:_:DarkMagenta:90
|
||||
Grey27:238:_:DarkMagenta1:91
|
||||
Grey3:232:_:DarkViolet1:92
|
||||
|
@ -470,7 +446,7 @@ HotPink2:169:_:LightGreen:119
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
HotPink3:132:_:LightGreen1:120
|
||||
HotPink4:168:_:PaleGreen1:121
|
||||
IndianRed:131:_:Aquamarine1:122
|
||||
|
@ -506,7 +482,7 @@ LightSlateGrey:103:_:DarkOliveGreen6:149
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
LightSteelBlue:147:_:DarkSeaGreen6:150
|
||||
LightSteelBlue1:189:_:DarkSeaGreen3:151
|
||||
LightSteelBlue3:146:_:LightCyan3:152
|
||||
|
@ -542,7 +518,7 @@ NavajoWhite3:144:_:LightGoldenrod3:179
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
Navy:4:_:Tan:180
|
||||
NavyBlue:17:_:MistyRose3:181
|
||||
Olive:3:_:Thistle3:182
|
||||
|
@ -578,7 +554,7 @@ Purple5:55:_:Salmon1:209
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
Red:9:_:LightCoral:210
|
||||
Red1:196:_:PaleVioletRed1:211
|
||||
Red2:124:_:Orchid2:212
|
||||
|
@ -614,7 +590,7 @@ Tan:180:_:Grey30:239
|
|||
allbox tab(:);
|
||||
lb|lb|l|lb|lb
|
||||
l l|l|l l.
|
||||
name \(da:byte:_:name:byte \(da
|
||||
name ↓:byte:_:name:byte ↓
|
||||
Teal:6:_:Grey35:240
|
||||
Thistle1:225:_:Grey39:241
|
||||
Thistle3:182:_:Grey42:242
|
||||
|
@ -635,34 +611,15 @@ Yellow6:148:_:Grey93:255
|
|||
.Sh SEE ALSO
|
||||
.Xr meli 1 ,
|
||||
.Xr meli.conf 5
|
||||
.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 CONFORMING TO
|
||||
TOML Standard v.0.5.0 https://toml.io/en/v0.5.0
|
||||
.sp
|
||||
https://no-color.org/
|
||||
.Sh AUTHORS
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Copyright 2017-2019
|
||||
.An Manos Pitsidianakis Aq epilys@nessuent.xyz
|
||||
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
|
||||
(See COPYING for full copyright and warranty notices.)
|
||||
.Pp
|
||||
.Aq https://meli.delivery
|
|
@ -17,10 +17,6 @@
|
|||
.\" 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
|
||||
|
@ -44,13 +40,12 @@
|
|||
.Ed
|
||||
.sp
|
||||
..
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dd November 11, 2022
|
||||
.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
|
||||
|
@ -69,61 +64,29 @@ Start meli with given configuration file.
|
|||
Create configuration file in
|
||||
.Pa path
|
||||
if given, or at
|
||||
.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.
|
||||
.Pa $XDG_CONFIG_HOME/meli/config.toml
|
||||
.It Cm edit-config
|
||||
Edit configuration files with
|
||||
.Ev EDITOR
|
||||
or
|
||||
.Ev VISUAL Ns
|
||||
\&.
|
||||
.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 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 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-log-path
|
||||
Print log file location.
|
||||
.It Cm compiled-with
|
||||
Print compile time feature flags of this binary.
|
||||
.It Cm view
|
||||
View mail from input file.
|
||||
.El
|
||||
.Sh DESCRIPTION
|
||||
.Nm
|
||||
is a terminal mail client aiming for extensive and user-friendly configurability.
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
.Bd -literal
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -157,28 +120,11 @@ 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 \(ga listing toggle_menu_visibility
|
||||
.ShortcutPeriod ` listing toggle_menu_visibility
|
||||
\&.
|
||||
.Pp
|
||||
Each mailbox may be viewed in 4 modes:
|
||||
.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
|
||||
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.
|
||||
.Pp
|
||||
If you're using a light color palette in your terminal, you should set
|
||||
.Em theme = "light"
|
||||
|
@ -194,10 +140,6 @@ 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
|
||||
|
@ -214,32 +156,16 @@ See
|
|||
for the location of the mailcap files and
|
||||
.Xr mailcap 5
|
||||
for their syntax.
|
||||
You can save individual attachments with the following command:
|
||||
.Command save\-attachment Ar INDEX Ar path\-to\-file
|
||||
You can save individual attachments with the
|
||||
.Command save-attachment Ar INDEX Ar path-to-file
|
||||
command.
|
||||
.Ar INDEX
|
||||
is the attachment's index in the listing.
|
||||
.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.
|
||||
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.
|
||||
.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
|
||||
|
@ -266,7 +192,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 reindex Ar ACCOUNT_NAME Ns
|
||||
.Command index 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
|
||||
|
@ -288,8 +214,9 @@ alias:
|
|||
.Pc
|
||||
String keywords with spaces must be quoted.
|
||||
Quotes should always be escaped.
|
||||
.Ss Important Notice about IMAP/JMAP
|
||||
.HorizontalRule
|
||||
.sp
|
||||
.Sy Important Notice about IMAP/JMAP
|
||||
.sp
|
||||
To prevent downloading all your messages from your IMAP/JMAP server, don't set
|
||||
.Em search_backend
|
||||
to
|
||||
|
@ -298,17 +225,14 @@ 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 noticeable delay.
|
||||
Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticable delay.
|
||||
.Ss QUERY ABNF SYNTAX
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.Bl -bullet
|
||||
.It
|
||||
.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
|
||||
.Li query = \&"(\&" query \&")\&" | from | to | cc | bcc | alladdresses | subject | flags | has_attachments | 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
|
||||
|
@ -319,8 +243,6 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.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
|
||||
|
@ -329,39 +251,16 @@ Sqlite3 on the contrary at reasonable mailbox sizes should have a non noticeable
|
|||
.It
|
||||
.Li bcc = \&"bcc:\&" term
|
||||
.It
|
||||
.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
|
||||
.Li alladdresses = \&"alladdresses:\&" 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
|
||||
.Ns Ql tags:
|
||||
or
|
||||
.Ns Ql flags:
|
||||
prefix in a search query, and can be modified by
|
||||
Tags can be searched with the `tags:` or `flags:` prefix in a search query, and can be modified by
|
||||
.Command tag add TAG
|
||||
and
|
||||
.Command tag remove TAG
|
||||
|
@ -382,8 +281,7 @@ To reply to a mail, press
|
|||
\&.
|
||||
Both these actions open the mail composer view in a new tab.
|
||||
.Ss Editing text
|
||||
.HorizontalRule
|
||||
.Bl -dash -compact
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
Edit the header fields by selecting with the arrow keys and pressing
|
||||
.Shortcut Enter general focus_in_text_field
|
||||
|
@ -426,14 +324,12 @@ 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
|
||||
|
@ -451,7 +347,6 @@ 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
|
||||
|
@ -463,10 +358,11 @@ 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:
|
||||
.Bl -enum -compact
|
||||
.sp
|
||||
.Bl -enum -compact -offset indent
|
||||
.It
|
||||
an internal format that gets saved under
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/contacts Ns
|
||||
.Pa $XDG_DATA_HOME/meli/account_name/addressbook Ns
|
||||
\&.
|
||||
.It
|
||||
vCard files (v3, v4) through the
|
||||
|
@ -485,7 +381,7 @@ compatible alias file in the option
|
|||
.sp
|
||||
See
|
||||
.Xr meli.conf 5 ACCOUNTS
|
||||
for the complete account contact configuration values.
|
||||
for the complete account configuration values.
|
||||
.Sh MODES
|
||||
.Bl -tag -compact -width 8n
|
||||
.It NORMAL
|
||||
|
@ -505,9 +401,8 @@ captures all input as text input, and is exited with
|
|||
.Cm Esc
|
||||
key.
|
||||
.El
|
||||
.Sh COMMAND
|
||||
.Ss COMMAND Mode
|
||||
.Ss Mail listing commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm set Ar plain | threaded | compact | conversations
|
||||
set the way mailboxes are displayed
|
||||
|
@ -521,9 +416,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 Oo Ar subject | date Oc Ar asc | desc
|
||||
.It Cm sort Ar subject | date \ Ar asc | desc
|
||||
sort mail listing
|
||||
.It Cm subsort Oo Ar subject | date Oc Ar asc | desc
|
||||
.It Cm subsort Ar subject | date \ Ar asc | desc
|
||||
sorts only the first level of replies.
|
||||
.It Cm go Ar n
|
||||
where
|
||||
|
@ -542,49 +437,37 @@ Escape exits search results.
|
|||
select threads matching
|
||||
.Ar STRING
|
||||
query.
|
||||
.It Cm clear-selection
|
||||
Clear current selection.
|
||||
.It Cm set Ar seen | unseen
|
||||
.It Cm set seen, set 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 entries.
|
||||
Delete selected threads.
|
||||
.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 irreversible.
|
||||
This action is unreversible.
|
||||
.El
|
||||
.Ss Mail view commands
|
||||
.HorizontalRule
|
||||
.Bl -tag -width 36n
|
||||
.It Cm pipe Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
.It Cm pipe Ar EXECUTABLE Ar ARGS
|
||||
pipe pager contents to binary
|
||||
.It Cm filter Ar EXECUTABLE Oo Ar ARGS Oc
|
||||
.It Cm filter Ar EXECUTABLE Ar ARGS
|
||||
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
|
||||
|
@ -593,12 +476,11 @@ unsubscribe automatically from list of viewed envelope
|
|||
open list archive with
|
||||
.Cm xdg-open
|
||||
.El
|
||||
.Ss Composing mail commands
|
||||
.HorizontalRule
|
||||
.Ss composing mail commands
|
||||
.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
|
||||
|
@ -617,11 +499,7 @@ in
|
|||
Launch command
|
||||
.Ar CMD Ar ARGS Ns
|
||||
\&.
|
||||
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
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
|
@ -633,8 +511,7 @@ for PGP configuration.
|
|||
.It Cm save-draft
|
||||
saves a copy of the draft in the Draft folder
|
||||
.El
|
||||
.Ss Generic commands
|
||||
.HorizontalRule
|
||||
.Ss generic commands
|
||||
.Bl -tag -width 36n
|
||||
.It Cm open-in-tab
|
||||
opens envelope view in new tab
|
||||
|
@ -658,6 +535,10 @@ 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.
|
||||
|
@ -669,15 +550,13 @@ catchall for general errors
|
|||
process panic
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.It Ev EDITOR
|
||||
Specifies the editor to use
|
||||
.It Ev MELI_CONFIG
|
||||
Override the configuration file
|
||||
.It Ev NO_COLOR
|
||||
When defined (regardless of its value), prevents the addition of
|
||||
.Em ANSI
|
||||
color.
|
||||
When present (regardless of its value), prevents the addition of ANSI color.
|
||||
The configuration value
|
||||
.Ic use_color
|
||||
overrides this.
|
||||
|
@ -685,7 +564,7 @@ overrides this.
|
|||
.Sh FILES
|
||||
.Nm
|
||||
uses the following parts of the XDG standard:
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -offset indent
|
||||
.It Ev XDG_CONFIG_HOME
|
||||
defaults to
|
||||
.Pa ~/.config/
|
||||
|
@ -695,13 +574,17 @@ defaults to
|
|||
.El
|
||||
.Pp
|
||||
and appropriates the following locations:
|
||||
.Bl -tag -width "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -offset indent
|
||||
.Bl -tag -width "$XDG_CONFIG_HOME/meli/plugins/*" -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/*
|
||||
|
@ -730,281 +613,75 @@ 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 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
|
||||
.Sh CONFORMING TO
|
||||
.Bl -bullet -compact
|
||||
.It
|
||||
.Lk https://meli\-email.org "Website"
|
||||
XDG Standard
|
||||
.Lk https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html Ns
|
||||
\&.
|
||||
.It
|
||||
.Lk https://git.meli\-email.org/meli/meli "Main\ git\ repository\ and\ issue\ tracker"
|
||||
mailcap file, RFC 1524: A User Agent Configuration Mechanism For Multimedia Mail Format Information
|
||||
.It
|
||||
.Lk https://codeberg.org/meli/meli "Official\ read-only\ git\ mirror\ on\ codeberg.org"
|
||||
RFC 5322: Internet Message Format
|
||||
.It
|
||||
.Lk https://github.com/meli/meli "Official\ read-only\ git\ mirror\ on\ github.com"
|
||||
RFC 6532: Internationalized Email Headers
|
||||
.It
|
||||
.Lk https://crates.io/crates/meli "meli\ crate\ on\ crates.io"
|
||||
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 Mt 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).
|
||||
.Pp
|
||||
.Lk https://meli.delivery
|
|
@ -40,23 +40,22 @@
|
|||
.Pc Ns
|
||||
..
|
||||
.de Command
|
||||
.Bd -ragged -offset 1n
|
||||
.Bd -offset 1n -ragged
|
||||
.Cm \\$*
|
||||
.Ed
|
||||
..
|
||||
.\".Dd November 11, 2022
|
||||
.Dd March 10, 2024
|
||||
.Dd November 11, 2022
|
||||
.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\-friendly configurability.
|
||||
is a terminal mail client aiming for extensive and user-frendly configurability.
|
||||
.Bd -literal -offset center
|
||||
^^ .-=-=-=-. ^^
|
||||
^^ (`-=-=-=-=-`) ^^
|
||||
|
@ -159,9 +158,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.
|
||||
|
@ -230,7 +229,7 @@ This is the view you will spend more time with in
|
|||
\&.
|
||||
.Pp
|
||||
Press
|
||||
.Shortcut \(ga listing toggle_menu_visibility
|
||||
.Shortcut ` listing toggle_menu_visibility
|
||||
to toggle the sidebars visibility.
|
||||
.Pp
|
||||
Press
|
||||
|
@ -238,16 +237,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
|
||||
|
@ -295,9 +294,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.
|
||||
|
@ -311,7 +310,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
|
||||
|
@ -331,7 +330,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
|
||||
|
@ -344,16 +343,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\&.
|
||||
|
@ -412,9 +411,9 @@ which shows more than one line per thread which can include multiple e\-mails wi
|
|||
.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.
|
||||
.Shortcut v listing select_motion
|
||||
.Qq select_entry
|
||||
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:
|
||||
|
@ -422,13 +421,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
|
||||
|
@ -446,11 +445,7 @@ which also has its complement
|
|||
.sp
|
||||
action.
|
||||
.Pp
|
||||
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
|
||||
For e-mail backends that support tags
|
||||
.Po
|
||||
like
|
||||
.Qq IMAP
|
||||
|
@ -468,13 +463,10 @@ 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 ▀│
|
||||
|
@ -502,14 +494,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.
|
||||
|
@ -524,7 +516,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
|
||||
|
@ -555,13 +547,13 @@ for more details
|
|||
.Pc Ns
|
||||
\&.
|
||||
.Sh MAIL VIEW
|
||||
Other things you can do when viewing e\-mail:
|
||||
.Bl -dash -compact
|
||||
Other things you can do when viewing e-mail:
|
||||
.Bl -bullet -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:
|
||||
|
@ -577,39 +569,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 e\-mail:
|
||||
.Shortcut Ctrl\-f envelope_view forward
|
||||
Forward email:
|
||||
.Shortcut Ctrl-f envelope_view forward
|
||||
.It
|
||||
Expand extra headers: (References and others)
|
||||
.Shortcut h envelope_view toggle_expand_headers
|
||||
.Shortcut h envelope_view toggle_expand_headerk
|
||||
.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 -dash -compact
|
||||
.Bl -bullet -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,7 +680,7 @@ If you enable the embed terminal option, you can launch your terminal editor of
|
|||
.Ed
|
||||
.Bd -ragged -offset 3n
|
||||
.Bf -emphasis
|
||||
.Xr nvim 1 Ns
|
||||
.Xr neovim 1 Ns
|
||||
\ running\ inside\ the\ composing\ tab\&.
|
||||
.Ef
|
||||
The\ double\ line\ border\ annotates\ the\ area\ of\ the\ embedded\ terminal,
|
||||
|
@ -696,29 +688,25 @@ 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 stdout, separated by NUL bytes.
|
||||
Example usage with
|
||||
.Xr fzf Ns
|
||||
:
|
||||
.D1 add-attachment-file-picker < fzf --print0
|
||||
.It Cm remove\-attachment Ar INDEX
|
||||
The command should print file paths in stderr, separated by NULL bytes.
|
||||
.It Cm remove-attachment Ar INDEX
|
||||
remove attachment with given index
|
||||
.It Cm toggle sign
|
||||
toggle between signing and not signing this message.
|
||||
|
@ -726,10 +714,10 @@ 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
|
||||
.\" TODO add contacts section
|
||||
.Sh THEMES
|
||||
See
|
||||
.Xr meli-themes 5
|
||||
|
@ -743,26 +731,12 @@ for documentation on how to theme
|
|||
.Xr xdg-open 1 ,
|
||||
.Xr mailcap 5
|
||||
.Sh AUTHORS
|
||||
Copyright 2017\(en2024
|
||||
.An Manos Pitsidianakis Aq Mt manos@pitsidianak.is
|
||||
.Pp
|
||||
Copyright 2017-2022
|
||||
.An Manos Pitsidianakis Mt manos@pitsidianak.is
|
||||
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
|
||||
(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
|
|
@ -11,12 +11,9 @@
|
|||
#[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
|
||||
|
@ -29,7 +26,6 @@
|
|||
#[accounts.mbox]
|
||||
#root_mailbox = "/var/mail/username"
|
||||
#format = "mbox"
|
||||
#send_mail = 'false'
|
||||
#listing.index_style = "Compact"
|
||||
#identity="username@hostname.local"
|
||||
#
|
||||
|
@ -37,7 +33,6 @@
|
|||
#[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"
|
||||
|
@ -47,14 +42,15 @@
|
|||
#listing.index_style = "Conversations"
|
||||
#identity = "username@example.com"
|
||||
#display_name = "Name Name"
|
||||
### show only specific mailboxes, overriding the server's subscribed status.
|
||||
### match every mailbox:
|
||||
#subscribed_mailboxes = ["*" ]
|
||||
### match specific mailboxes:
|
||||
##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"
|
||||
|
@ -68,7 +64,6 @@
|
|||
#[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"
|
||||
|
@ -76,18 +71,22 @@
|
|||
#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:
|
||||
### 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:
|
||||
#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"
|
||||
|
@ -129,8 +128,12 @@
|
|||
#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
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 204 KiB After Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
@ -8,14 +8,17 @@ edition = "2018"
|
|||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[[bin]]
|
||||
name = "envelope_parse"
|
||||
path = "fuzz_targets/envelope_parse.rs"
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.3"
|
||||
melib = { path = "../melib" }
|
||||
|
||||
[dependencies.melib]
|
||||
path = "../melib"
|
||||
features = ["unicode_algorithms"]
|
||||
|
||||
# Prevent this from interfering with workspaces
|
||||
[workspace]
|
||||
members = ["."]
|
||||
|
||||
[[bin]]
|
||||
name = "envelope_parse"
|
||||
path = "fuzz_targets/envelope_parse.rs"
|
||||
|
|
|
@ -1,91 +0,0 @@
|
|||
[package]
|
||||
name = "meli"
|
||||
version = "0.8.10"
|
||||
authors = ["Manos Pitsidianakis <manos@pitsidianak.is>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70.0"
|
||||
license = "EUPL-1.2 OR GPL-3.0-or-later"
|
||||
readme = "README.md"
|
||||
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"
|
||||
path = "src/main.rs"
|
||||
|
||||
[lib]
|
||||
name = "meli"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
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 = { 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.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.26", default-features = false }
|
||||
# svg_crate = { version = "^0.13", optional = true, package = "svg" }
|
||||
termion = { version = "1.5.1", default-features = false }
|
||||
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", "static"]
|
||||
notmuch = ["melib/notmuch"]
|
||||
jmap = ["melib/jmap"]
|
||||
sqlite3 = ["melib/sqlite3"]
|
||||
smtp = ["melib/smtp"]
|
||||
smtp-trace = ["smtp", "melib/smtp-trace"]
|
||||
dbus-notifications = ["dep:notify-rust"]
|
||||
cli-docs = ["dep:flate2"]
|
||||
# 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"]
|
||||
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 }
|
|
@ -1 +0,0 @@
|
|||
../README.md
|
|
@ -1,42 +0,0 @@
|
|||
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.
|
|
@ -1,239 +0,0 @@
|
|||
.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)
|
|
@ -1,227 +0,0 @@
|
|||
.\" 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"
|
|
@ -1,73 +0,0 @@
|
|||
[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"
|
1894
meli/src/accounts.rs
|
@ -1,92 +0,0 @@
|
|||
/*
|
||||
* meli - accounts module.
|
||||
*
|
||||
* Copyright 2023 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/>.
|
||||
*/
|
||||
|
||||
//! Account mail backend operations.
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Account {
|
||||
pub fn set_flags(
|
||||
&mut self,
|
||||
env_hashes: EnvelopeHashBatch,
|
||||
mailbox_hash: MailboxHash,
|
||||
flags: SmallVec<[FlagOp; 8]>,
|
||||
) -> Result<JobId> {
|
||||
let fut = self.backend.write().unwrap().set_flags(
|
||||
env_hashes.clone(),
|
||||
mailbox_hash,
|
||||
flags.clone(),
|
||||
)?;
|
||||
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,
|
||||
JobRequest::SetFlags {
|
||||
env_hashes,
|
||||
mailbox_hash,
|
||||
flags,
|
||||
handle,
|
||||
},
|
||||
);
|
||||
|
||||
Ok(job_id)
|
||||
}
|
||||
|
||||
// #[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 == 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,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,247 +0,0 @@
|
|||
//
|
||||
// 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(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
//
|
||||
// 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);
|
||||
}
|
||||
}
|
|
@ -1,439 +0,0 @@
|
|||
//
|
||||
// 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),
|
||||
}));
|
||||
}
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,565 +0,0 @@
|
|||
//
|
||||
// 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();
|
||||
}
|
||||
}
|
269
meli/src/args.rs
|
@ -1,269 +0,0 @@
|
|||
/*
|
||||
* meli - args.rs
|
||||
*
|
||||
* Copyright 2017-2023 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/>.
|
||||
*/
|
||||
|
||||
//! 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")]
|
||||
pub struct Opt {
|
||||
/// use specified configuration file
|
||||
#[structopt(short, long, parse(from_os_str))]
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
#[structopt(subcommand)]
|
||||
pub subcommand: Option<SubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum SubCommand {
|
||||
/// print default theme in full to stdout and exit.
|
||||
PrintDefaultTheme,
|
||||
/// print loaded themes in full to stdout and exit.
|
||||
PrintLoadedThemes,
|
||||
/// 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`. Path `-` will output to standard
|
||||
/// output instead.
|
||||
#[structopt(display_order = 1)]
|
||||
CreateConfig {
|
||||
#[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 = try_path_or_stdio))]
|
||||
path: Option<PathOrStdio>,
|
||||
},
|
||||
#[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 = 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 = 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))]
|
||||
path: PathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct ManOpt {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
|
||||
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())
|
||||
)
|
||||
);
|
||||
} 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(())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,565 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2017-2018 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/>.
|
||||
*/
|
||||
|
||||
//! A parser module for user commands passed through
|
||||
//! [`Command`](crate::types::UIMode::Command) mode.
|
||||
|
||||
use std::{borrow::Cow, collections::HashSet, str::FromStr};
|
||||
|
||||
use melib::{
|
||||
nom::{
|
||||
self,
|
||||
branch::alt,
|
||||
bytes::complete::{is_a, is_not, tag, take_until},
|
||||
character::complete::{digit1, not_line_ending},
|
||||
combinator::{map, map_res},
|
||||
error::Error as NomError,
|
||||
multi::separated_list1,
|
||||
sequence::{pair, preceded, separated_pair},
|
||||
IResult,
|
||||
},
|
||||
parser::BytesExt,
|
||||
SortField, SortOrder,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub mod actions;
|
||||
#[macro_use]
|
||||
pub mod error;
|
||||
#[macro_use]
|
||||
pub mod argcheck;
|
||||
pub mod history;
|
||||
pub mod parser;
|
||||
use actions::MailboxOperation;
|
||||
use error::CommandError;
|
||||
pub use parser::parse_command;
|
||||
|
||||
pub use crate::actions::{
|
||||
AccountAction::{self, *},
|
||||
Action::{self, *},
|
||||
ComposeAction::{self, *},
|
||||
ComposerTabAction, FlagAction,
|
||||
ListingAction::{self, *},
|
||||
MailingListAction::{self, *},
|
||||
TabAction::{self, *},
|
||||
TagAction,
|
||||
ViewAction::{self, *},
|
||||
};
|
||||
|
||||
/// Helper macro to convert an array of tokens into a `TokenStream`
|
||||
macro_rules! to_stream {
|
||||
($token: expr) => {
|
||||
TokenStream {
|
||||
tokens: &[$token],
|
||||
}
|
||||
};
|
||||
($($tokens:expr),*) => {
|
||||
TokenStream {
|
||||
tokens: &[$($tokens),*],
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro to create a const table with every command part that can be
|
||||
/// auto-completed and its description
|
||||
macro_rules! define_commands {
|
||||
( [$({ tags: [$( $tags:literal),*], desc: $desc:literal, tokens: $tokens:expr, parser: $parser:path}),*]) => {
|
||||
pub const COMMAND_COMPLETION: &[(&str, &str, TokenStream, fn(&[u8]) -> IResult<&[u8], Result<Action, CommandError>>)] = &[$($( ($tags, $desc, TokenStream { tokens: $tokens }, $parser) ),*),* ];
|
||||
};
|
||||
}
|
||||
|
||||
pub fn quoted_argument(input: &[u8]) -> IResult<&[u8], &str> {
|
||||
if input.is_empty() {
|
||||
return Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}));
|
||||
}
|
||||
|
||||
if input[0] == b'"' {
|
||||
let mut i = 1;
|
||||
while i < input.len() {
|
||||
if input[i] == b'\"' && input[i - 1] != b'\\' {
|
||||
return Ok((&input[i + 1..], unsafe {
|
||||
std::str::from_utf8_unchecked(&input[1..i])
|
||||
}));
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}))
|
||||
} else {
|
||||
map_res(is_not(" "), std::str::from_utf8)(input)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct TokenStream {
|
||||
tokens: &'static [TokenAdicity],
|
||||
}
|
||||
|
||||
use Token::*;
|
||||
use TokenAdicity::*;
|
||||
|
||||
impl TokenStream {
|
||||
fn matches<'s>(&self, s: &mut &'s str, sugg: &mut HashSet<String>) -> Vec<(&'s str, Token)> {
|
||||
let mut tokens = vec![];
|
||||
for t in self.tokens.iter() {
|
||||
let mut ptr = 0;
|
||||
while ptr + 1 < s.len() && s.as_bytes()[ptr].is_ascii_whitespace() {
|
||||
ptr += 1;
|
||||
}
|
||||
*s = &s[ptr..];
|
||||
//println!("\t before s.is_empty() {:?} {:?}", t, s);
|
||||
if s.is_empty() || *s == " " {
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
sugg.insert(format!("{}{}", if s.is_empty() { " " } else { "" }, lit));
|
||||
}
|
||||
Alternatives(v) => {
|
||||
for t in v.iter() {
|
||||
//println!("adding empty suggestions for {:?}", t);
|
||||
let mut _s = *s;
|
||||
let mut m = t.matches(&mut _s, sugg);
|
||||
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());
|
||||
}
|
||||
t @ AttachmentIndexValue
|
||||
| t @ MailboxIndexValue
|
||||
| t @ IndexValue
|
||||
| t @ Filepath
|
||||
| t @ AccountName
|
||||
| t @ MailboxPath
|
||||
| t @ QuotedStringValue
|
||||
| t @ AlphanumericStringValue => {
|
||||
let _t = t;
|
||||
//sugg.insert(format!("{}{:?}", if s.is_empty() { " " }
|
||||
// else { "" }, t));
|
||||
}
|
||||
}
|
||||
tokens.push((*s, *t.inner()));
|
||||
return tokens;
|
||||
}
|
||||
match t.inner() {
|
||||
Literal(lit) => {
|
||||
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![];
|
||||
}
|
||||
}
|
||||
Alternatives(v) => {
|
||||
let mut cont = true;
|
||||
for t in v.iter() {
|
||||
let mut _s = *s;
|
||||
let mut m = t.matches(&mut _s, sugg);
|
||||
if !m.is_empty() {
|
||||
tokens.append(&mut m);
|
||||
//println!("_s is empty {}", _s.is_empty());
|
||||
cont = !_s.is_empty();
|
||||
*s = _s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
return tokens;
|
||||
}
|
||||
if !cont {
|
||||
*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![];
|
||||
}
|
||||
RestOfStringValue => {
|
||||
tokens.push((*s, *t.inner()));
|
||||
return tokens;
|
||||
}
|
||||
AttachmentIndexValue
|
||||
| MailboxIndexValue
|
||||
| IndexValue
|
||||
| Filepath
|
||||
| AccountName
|
||||
| MailboxPath
|
||||
| QuotedStringValue
|
||||
| AlphanumericStringValue => {
|
||||
let mut ptr = 0;
|
||||
while ptr + 1 < s.len() && !s.as_bytes()[ptr].is_ascii_whitespace() {
|
||||
ptr += 1;
|
||||
}
|
||||
tokens.push((&s[..ptr + 1], *t.inner()));
|
||||
*s = &s[ptr + 1..];
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens
|
||||
}
|
||||
}
|
||||
|
||||
/// `Token` wrapper that defines how many times a token is expected to be
|
||||
/// repeated
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TokenAdicity {
|
||||
ZeroOrOne(Token),
|
||||
ZeroOrMore(Token),
|
||||
One(Token),
|
||||
OneOrMore(Token),
|
||||
}
|
||||
|
||||
impl TokenAdicity {
|
||||
fn inner(&self) -> &Token {
|
||||
match self {
|
||||
ZeroOrOne(ref t) => t,
|
||||
ZeroOrMore(ref t) => t,
|
||||
One(ref t) => t,
|
||||
OneOrMore(ref t) => t,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A token encountered in the UI's command execution bar
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Token {
|
||||
Literal(&'static str),
|
||||
Filepath,
|
||||
Alternatives(&'static [TokenStream]),
|
||||
AlternativeStrings(&'static [&'static str]),
|
||||
Seq(&'static [TokenAdicity]),
|
||||
AccountName,
|
||||
MailboxPath,
|
||||
QuotedStringValue,
|
||||
RestOfStringValue,
|
||||
AlphanumericStringValue,
|
||||
AttachmentIndexValue,
|
||||
MailboxIndexValue,
|
||||
IndexValue,
|
||||
}
|
||||
|
||||
fn eof(input: &[u8]) -> IResult<&[u8], ()> {
|
||||
if input.is_empty() {
|
||||
Ok((input, ()))
|
||||
} else {
|
||||
Err(nom::Err::Error(NomError {
|
||||
input,
|
||||
code: nom::error::ErrorKind::Tag,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
define_commands!([
|
||||
{ tags: ["set", "set seen", "set unseen", "set plain", "set threaded", "set compact"],
|
||||
desc: "set [seen/unseen], toggles message's Seen flag. set [plain/threaded/compact/conversations] changes the mail listing view",
|
||||
tokens: &[One(Literal("set")),
|
||||
One(
|
||||
Alternatives(&[
|
||||
to_stream!(One(Literal("seen"))),
|
||||
to_stream!(One(Literal("unseen"))),
|
||||
to_stream!(One(Literal("plain"))),
|
||||
to_stream!(One(Literal("threaded"))),
|
||||
to_stream!(One(Literal("compact"))),
|
||||
to_stream!(One(Literal("conversations")))
|
||||
])
|
||||
)
|
||||
],
|
||||
parser: parser::set
|
||||
},
|
||||
{ tags: ["delete"],
|
||||
desc: "delete message",
|
||||
tokens: &[One(Literal("delete"))],
|
||||
parser: parser::delete_message
|
||||
},
|
||||
{ tags: ["copyto", "moveto"],
|
||||
desc: "copy/move message",
|
||||
tokens: &[One(Alternatives(&[to_stream!(One(Literal("copyto"))), to_stream!(One(Literal("moveto")))])), ZeroOrOne(AccountName), One(MailboxPath)],
|
||||
parser: parser::copymove
|
||||
},
|
||||
{ tags: ["import "],
|
||||
desc: "import FILESYSTEM_PATH MAILBOX_PATH",
|
||||
tokens: &[One(Literal("import")), One(Filepath), One(MailboxPath)],
|
||||
parser: parser::import
|
||||
},
|
||||
{ tags: ["close"],
|
||||
desc: "close non-sticky tabs",
|
||||
tokens: &[One(Literal("close"))],
|
||||
parser: parser::close
|
||||
},
|
||||
{ tags: ["go"],
|
||||
desc: "go <n>, switch to nth mailbox in this account",
|
||||
tokens: &[One(Literal("goto")), One(MailboxIndexValue)],
|
||||
parser: parser::goto
|
||||
},
|
||||
{ tags: ["subsort"],
|
||||
desc: "subsort [date/subject] [asc/desc], sorts first level replies in threads.",
|
||||
tokens: &[One(Literal("subsort")), One(Alternatives(&[to_stream!(One(Literal("date"))), to_stream!(One(Literal("subject")))])), One(Alternatives(&[to_stream!(One(Literal("asc"))), to_stream!(One(Literal("desc")))])) ],
|
||||
parser: parser::subsort
|
||||
},
|
||||
{ tags: ["sort"],
|
||||
desc: "sort [date/subject] [asc/desc], sorts threads.",
|
||||
tokens: &[One(Literal("sort")), One(Alternatives(&[to_stream!(One(Literal("date"))), to_stream!(One(Literal("subject")))])), One(Alternatives(&[to_stream!(One(Literal("asc"))), to_stream!(One(Literal("desc")))])) ],
|
||||
parser: parser::sort
|
||||
},
|
||||
{ tags: ["sort"],
|
||||
desc: "sort <column index> [asc/desc], sorts table columns.",
|
||||
tokens: &[One(Literal("sort")), One(IndexValue), ZeroOrOne(Alternatives(&[to_stream!(One(Literal("asc"))), to_stream!(One(Literal("desc")))])) ],
|
||||
parser: parser::sort_column
|
||||
},
|
||||
{ tags: ["toggle thread_snooze"],
|
||||
desc: "turn off new notifications for this thread",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("thread_snooze"))],
|
||||
parser: parser::toggle
|
||||
},
|
||||
{ tags: ["search"],
|
||||
desc: "search <TERM>, searches list with given term",
|
||||
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)],
|
||||
parser: parser::select
|
||||
},
|
||||
{ tags: ["export-mbox "],
|
||||
desc: "export-mbox PATH",
|
||||
tokens: &[One(Literal("export-mbox")), One(Filepath)],
|
||||
parser: parser::export_mbox
|
||||
},
|
||||
{ tags: ["list-archive", "list-post", "list-unsubscribe", "list-"],
|
||||
desc: "list-[unsubscribe/post/archive]",
|
||||
tokens: &[One(Alternatives(&[to_stream!(One(Literal("list-archive"))), to_stream!(One(Literal("list-post"))), to_stream!(One(Literal("list-unsubscribe")))]))],
|
||||
parser: parser::mailinglist
|
||||
},
|
||||
{ tags: ["setenv "],
|
||||
desc: "setenv VAR=VALUE",
|
||||
tokens: &[One(Literal("setenv")), OneOrMore(Seq(&[One(AlphanumericStringValue), One(Literal("=")), One(QuotedStringValue)]))],
|
||||
parser: parser::setenv
|
||||
},
|
||||
{ tags: ["printenv "],
|
||||
desc: "printenv VAR",
|
||||
tokens: &[],
|
||||
parser: parser::printenv
|
||||
},
|
||||
{ tags: ["mailto "],
|
||||
desc: "mailto MAILTO_ADDRESS",
|
||||
tokens: &[One(Literal("mailto")), One(QuotedStringValue)],
|
||||
parser: parser::mailto
|
||||
},
|
||||
/* Pipe pager contents to binary */
|
||||
{ tags: ["pipe "],
|
||||
desc: "pipe EXECUTABLE ARGS",
|
||||
tokens: &[One(Literal("pipe")), One(Filepath), ZeroOrMore(QuotedStringValue)],
|
||||
parser: parser::pipe
|
||||
},
|
||||
/* Filter pager contents through binary */
|
||||
{ tags: ["filter "],
|
||||
desc: "filter EXECUTABLE ARGS",
|
||||
tokens: &[One(Literal("filter")), One(Filepath), ZeroOrMore(QuotedStringValue)],
|
||||
parser: parser::filter
|
||||
},
|
||||
{ tags: ["add-attachment ", "add-attachment-file-picker "],
|
||||
desc: "add-attachment PATH",
|
||||
tokens: &[One(
|
||||
Alternatives(&[to_stream!(One(Literal("add-attachment")), One(Filepath)), to_stream!(One(Literal("add-attachment-file-picker")))]))],
|
||||
parser: parser::add_attachment
|
||||
},
|
||||
{ tags: ["remove-attachment "],
|
||||
desc: "remove-attachment INDEX",
|
||||
tokens: &[One(Literal("remove-attachment")), One(IndexValue)],
|
||||
parser: parser::remove_attachment
|
||||
},
|
||||
{ tags: ["save-draft"],
|
||||
desc: "save draft",
|
||||
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"))],
|
||||
parser: parser::toggle
|
||||
},
|
||||
{ tags: ["toggle encrypt"],
|
||||
desc: "toggle encryption for this draft",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("encrypt"))],
|
||||
parser: parser::toggle
|
||||
},
|
||||
{ tags: ["create-mailbox "],
|
||||
desc: "create-mailbox ACCOUNT MAILBOX_PATH",
|
||||
tokens: &[One(Literal("create-mailbox")), One(AccountName), One(MailboxPath)],
|
||||
parser: parser::create_mailbox
|
||||
},
|
||||
{ tags: ["subscribe-mailbox "],
|
||||
desc: "subscribe-mailbox ACCOUNT MAILBOX_PATH",
|
||||
tokens: &[One(Literal("subscribe-mailbox")), One(AccountName), One(MailboxPath)],
|
||||
parser: parser::sub_mailbox
|
||||
},
|
||||
{ tags: ["unsubscribe-mailbox "],
|
||||
desc: "unsubscribe-mailbox ACCOUNT MAILBOX_PATH",
|
||||
tokens: &[One(Literal("unsubscribe-mailbox")), One(AccountName), One(MailboxPath)],
|
||||
parser: parser::unsub_mailbox
|
||||
},
|
||||
{ tags: ["rename-mailbox "],
|
||||
desc: "rename-mailbox ACCOUNT MAILBOX_PATH_SRC MAILBOX_PATH_DEST",
|
||||
tokens: &[One(Literal("rename-mailbox")), One(AccountName), One(MailboxPath), One(MailboxPath)],
|
||||
parser: parser::rename_mailbox
|
||||
},
|
||||
{ tags: ["delete-mailbox "],
|
||||
desc: "delete-mailbox ACCOUNT MAILBOX_PATH",
|
||||
tokens: &[One(Literal("delete-mailbox")), One(AccountName), One(MailboxPath)],
|
||||
parser: parser::delete_mailbox
|
||||
},
|
||||
{ tags: ["reindex "],
|
||||
desc: "reindex ACCOUNT, rebuild account cache in the background",
|
||||
tokens: &[One(Literal("reindex")), One(AccountName)],
|
||||
parser: parser::reindex
|
||||
},
|
||||
{ tags: ["open-in-tab"],
|
||||
desc: "opens envelope view in new tab",
|
||||
tokens: &[One(Literal("open-in-tab"))],
|
||||
parser: parser::open_in_new_tab
|
||||
},
|
||||
{ tags: ["save-attachment "],
|
||||
desc: "save-attachment INDEX PATH",
|
||||
tokens: &[One(Literal("save-attachment")), One(AttachmentIndexValue), One(Filepath)],
|
||||
parser: parser::save_attachment
|
||||
},
|
||||
{ tags: ["export-mail "],
|
||||
desc: "export-mail PATH",
|
||||
tokens: &[One(Literal("export-mail")), One(Filepath)],
|
||||
parser: parser::export_mail
|
||||
},
|
||||
{ tags: ["add-addresses-to-contacts "],
|
||||
desc: "add-addresses-to-contacts",
|
||||
tokens: &[One(Literal("add-addresses-to-contacts"))],
|
||||
parser: parser::add_addresses_to_contacts
|
||||
},
|
||||
{ tags: ["tag", "tag add", "tag remove"],
|
||||
desc: "tag [add/remove], edits message's tags.",
|
||||
tokens: &[One(Literal("tag")), One(Alternatives(&[to_stream!(One(Literal("add"))), to_stream!(One(Literal("remove")))]))],
|
||||
parser: parser::_tag
|
||||
},
|
||||
{ tags: ["print "],
|
||||
desc: "print ACCOUNT SETTING",
|
||||
tokens: &[One(Literal("print")), One(AccountName), One(QuotedStringValue)],
|
||||
parser: parser::print_account_setting
|
||||
},
|
||||
{ tags: ["print "],
|
||||
desc: "print SETTING",
|
||||
tokens: &[One(Literal("print")), One(QuotedStringValue)],
|
||||
parser: parser::print_setting
|
||||
},
|
||||
{ tags: ["toggle mouse"],
|
||||
desc: "toggle mouse support",
|
||||
tokens: &[One(Literal("toggle")), One(Literal("mouse"))],
|
||||
parser: parser::toggle
|
||||
},
|
||||
{ tags: ["manage-mailboxes"],
|
||||
desc: "view and manage mailbox preferences",
|
||||
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"))],
|
||||
parser: parser::manage_jobs
|
||||
},
|
||||
{ tags: ["quit"],
|
||||
desc: "quit meli",
|
||||
tokens: &[One(Literal("quit"))],
|
||||
parser: parser::quit
|
||||
},
|
||||
{ tags: ["reload-config"],
|
||||
desc: "reload configuration file",
|
||||
tokens: &[One(Literal("reload-config"))],
|
||||
parser: parser::reload_config
|
||||
}
|
||||
]);
|
||||
|
||||
/// Get command suggestions for input
|
||||
pub fn command_completion_suggestions(input: &str) -> Vec<String> {
|
||||
use crate::melib::ShellExpandTrait;
|
||||
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() {
|
||||
continue;
|
||||
}
|
||||
if let Some((s, Filepath)) = _m.last() {
|
||||
let p = std::path::Path::new(s);
|
||||
sugg.extend(p.complete(true, s.ends_with('/')).into_iter());
|
||||
}
|
||||
}
|
||||
sugg.into_iter()
|
||||
.map(|s| format!("{}{}", input, s.as_str()))
|
||||
.collect::<Vec<String>>()
|
||||
}
|
|
@ -1,180 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2017 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/>.
|
||||
*/
|
||||
|
||||
//! Helper type for showing the exact reason why a command was invalid.
|
||||
|
||||
use super::*;
|
||||
|
||||
pub enum ArgCheck<const MIN: u8, const MAX: u8> {
|
||||
Start { __func__: &'static str },
|
||||
BeforeArgument { so_far: u8, __func__: &'static str },
|
||||
Eof { so_far: u8, __func__: &'static str },
|
||||
}
|
||||
|
||||
impl<const MIN: u8, const MAX: u8> ArgCheck<MIN, MAX> {
|
||||
#[inline]
|
||||
pub fn new(__func__: &'static str) -> Self {
|
||||
Self::Start { __func__ }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn start(&mut self, input: &[u8]) -> Result<(), CommandError> {
|
||||
let Self::Start { __func__ } = *self else {
|
||||
unreachable!(
|
||||
"ArgCheck::start called with invalid variant: {}",
|
||||
if matches!(self, Self::BeforeArgument { .. }) {
|
||||
"BeforeArgument"
|
||||
} else {
|
||||
"Eof"
|
||||
}
|
||||
);
|
||||
};
|
||||
let is_empty = input.trim().is_empty();
|
||||
if is_empty && MIN > 0 {
|
||||
return Err(CommandError::WrongNumberOfArguments {
|
||||
too_many: false,
|
||||
takes: (MIN, MAX.into()),
|
||||
given: 0,
|
||||
__func__,
|
||||
inner: format!(
|
||||
"needs {}{} arguments.",
|
||||
if MIN == MAX { "at least " } else { "" },
|
||||
MIN
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
*self = Self::BeforeArgument {
|
||||
so_far: 0,
|
||||
__func__,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inc(&mut self, input: &[u8]) -> Result<(), CommandError> {
|
||||
let Self::BeforeArgument { __func__, so_far } = *self else {
|
||||
unreachable!(
|
||||
"ArgCheck::inc called with invalid variant: {}",
|
||||
if matches!(self, Self::Start { .. }) {
|
||||
"Start"
|
||||
} else {
|
||||
"Eof"
|
||||
}
|
||||
);
|
||||
};
|
||||
let is_empty = input.trim().is_empty();
|
||||
let new_value = so_far + 1;
|
||||
if is_empty && new_value > MAX {
|
||||
return Err(CommandError::WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (MIN, MAX.into()),
|
||||
given: new_value,
|
||||
__func__,
|
||||
inner: format!(
|
||||
"needs {}{} arguments.",
|
||||
if MIN == MAX { "at least " } else { "" },
|
||||
MIN
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
*self = Self::BeforeArgument {
|
||||
so_far: new_value,
|
||||
__func__,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn finish(&mut self, input: &[u8]) -> Result<(), CommandError> {
|
||||
let Self::BeforeArgument { __func__, so_far } = *self else {
|
||||
unreachable!(
|
||||
"ArgCheck::finish called with invalid variant: {}",
|
||||
if matches!(self, Self::Start { .. }) {
|
||||
"Start"
|
||||
} else {
|
||||
"Eof"
|
||||
}
|
||||
);
|
||||
};
|
||||
let is_empty = input.trim().is_empty();
|
||||
if !is_empty {
|
||||
assert!(so_far <= MAX);
|
||||
assert!(so_far >= MIN);
|
||||
return Err(CommandError::WrongNumberOfArguments {
|
||||
too_many: true,
|
||||
takes: (MIN, MAX.into()),
|
||||
given: so_far + 1,
|
||||
__func__,
|
||||
inner: format!(
|
||||
"needs {}{} arguments.",
|
||||
if MIN == MAX { "at least " } else { "" },
|
||||
MIN
|
||||
)
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
*self = Self::Eof { so_far, __func__ };
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! arg_init {
|
||||
(min_arg: $n:expr, max_arg: $x:expr, $func:tt) => {{
|
||||
ArgCheck::<$n, $x>::new(stringify!($func))
|
||||
}};
|
||||
}
|
||||
|
||||
//macro_rules! arg_value_check {
|
||||
// ($tag:literal, $input:expr) => {{
|
||||
// if tag::<&'_ str, &'_ [u8],
|
||||
// melib::nom::error::Error<&[u8]>>($tag)($input).is_err() { return
|
||||
// Ok(( $input,
|
||||
// Err(CommandError::BadValue {
|
||||
// inner: $tag.to_string().into(),
|
||||
// }),
|
||||
// ));
|
||||
// }
|
||||
// tag($tag)($input)
|
||||
// }};
|
||||
//}
|
||||
|
||||
macro_rules! arg_chk {
|
||||
(start $check:ident, $input:expr) => {{
|
||||
if let Err(err) = $check.start($input) {
|
||||
return Ok(($input, Err(err)));
|
||||
};
|
||||
}};
|
||||
(inc $check:ident, $input:expr) => {{
|
||||
if let Err(err) = $check.inc($input) {
|
||||
return Ok(($input, Err(err)));
|
||||
};
|
||||
}};
|
||||
(finish $check:ident, $input:expr) => {{
|
||||
if let Err(err) = $check.finish($input) {
|
||||
return Ok(($input, Err(err)));
|
||||
};
|
||||
}};
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2017 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/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CommandError {
|
||||
Parsing {
|
||||
inner: Cow<'static, str>,
|
||||
kind: Cow<'static, str>,
|
||||
},
|
||||
BadValue {
|
||||
inner: Cow<'static, str>,
|
||||
suggestions: Option<&'static [&'static str]>,
|
||||
},
|
||||
WrongNumberOfArguments {
|
||||
too_many: bool,
|
||||
takes: (u8, Option<u8>),
|
||||
given: u8,
|
||||
__func__: &'static str,
|
||||
inner: Cow<'static, str>,
|
||||
},
|
||||
Other {
|
||||
inner: Cow<'static, str>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<nom::Err<melib::nom::error::Error<&'a [u8]>>> for CommandError {
|
||||
fn from(res: nom::Err<melib::nom::error::Error<&'a [u8]>>) -> Self {
|
||||
match res {
|
||||
nom::Err::Incomplete(_) => Self::Parsing {
|
||||
inner: res.to_string().into(),
|
||||
kind: "".into(),
|
||||
},
|
||||
nom::Err::Error(e) | nom::Err::Failure(e) => Self::Parsing {
|
||||
inner: String::from_utf8_lossy(e.input).to_string().into(),
|
||||
kind: format!("{:?}", e.code).into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for CommandError {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Parsing { inner, kind: _ } => {
|
||||
write!(fmt, "Could not parse command: {}", 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 {
|
||||
too_many,
|
||||
takes,
|
||||
given,
|
||||
__func__,
|
||||
inner: _,
|
||||
} => {
|
||||
if *too_many {
|
||||
match takes {
|
||||
(min, None) => {
|
||||
write!(
|
||||
fmt,
|
||||
"{}: Too many arguments. Command takes {} arguments, but {} were \
|
||||
given.",
|
||||
__func__, min, given
|
||||
)
|
||||
}
|
||||
(min, Some(max)) => {
|
||||
write!(
|
||||
fmt,
|
||||
"{}: Too many arguments. Command takes from {} to {} arguments, \
|
||||
but {} were given.",
|
||||
__func__, min, max, given
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
match takes {
|
||||
(min, None) => {
|
||||
write!(
|
||||
fmt,
|
||||
"{}: Not enough arguments. Command takes {} arguments, but {} \
|
||||
were given.",
|
||||
__func__, min, given
|
||||
)
|
||||
}
|
||||
(min, Some(max)) => {
|
||||
write!(
|
||||
fmt,
|
||||
"{}: Not enough arguments. Command takes from {} to {} arguments, \
|
||||
but {} were given.",
|
||||
__func__, min, max, given
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::Other { inner } => {
|
||||
write!(fmt, "Error: {}", inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for CommandError {}
|
|
@ -1,206 +0,0 @@
|
|||
//
|
||||
// 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"
|
||||
);
|
||||
}
|
769
meli/src/conf.rs
|
@ -1,769 +0,0 @@
|
|||
/*
|
||||
* meli - configuration module.
|
||||
*
|
||||
* Copyright 2017 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/>.
|
||||
*/
|
||||
|
||||
//! Configuration logic and `config.toml` interfaces.
|
||||
|
||||
extern crate serde;
|
||||
extern crate toml;
|
||||
extern crate xdg;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::OpenOptions,
|
||||
io::Write,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use melib::{
|
||||
backends::{MailboxHash, TagHash},
|
||||
conf::{ActionFlag, MailboxConf, ToggleFlag},
|
||||
error::*,
|
||||
search::Query,
|
||||
ShellExpandTrait, SortField, SortOrder, StderrLogger,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{conf::deserializers::non_empty_opt_string, terminal::Color};
|
||||
|
||||
pub mod default_values;
|
||||
pub mod preprocessing;
|
||||
use preprocessing as pp;
|
||||
|
||||
pub mod data_types;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
#[rustfmt::skip]
|
||||
mod overrides;
|
||||
pub use overrides::*;
|
||||
pub mod composing;
|
||||
pub mod notifications;
|
||||
pub mod pager;
|
||||
pub mod pgp;
|
||||
pub mod tags;
|
||||
#[macro_use]
|
||||
pub mod shortcuts;
|
||||
mod listing;
|
||||
pub mod terminal;
|
||||
mod themes;
|
||||
use default_values::*;
|
||||
pub use themes::*;
|
||||
|
||||
pub use self::{composing::*, pgp::*, shortcuts::*, tags::*};
|
||||
|
||||
/// Utility macro to access an [`AccountConf`] setting field from
|
||||
/// [`Context`](crate::Context) indexed by `$account_hash`
|
||||
///
|
||||
/// The value returned is the optionally overriden one in the
|
||||
/// [`AccountConf::conf_override`] field, otherwise the global one.
|
||||
///
|
||||
/// See also the [`mailbox_settings`](crate::mailbox_settings) macro.
|
||||
#[macro_export]
|
||||
macro_rules! account_settings {
|
||||
($context:ident[$account_hash:expr].$setting:ident.$field:ident) => {{
|
||||
$context.accounts[&$account_hash]
|
||||
.settings
|
||||
.conf_override
|
||||
.$setting
|
||||
.$field
|
||||
.as_ref()
|
||||
.unwrap_or(&$context.settings.$setting.$field)
|
||||
}};
|
||||
($context:ident[$account_hash:expr].$field:ident) => {{
|
||||
&$context.accounts[&$account_hash].settings.$field
|
||||
}};
|
||||
}
|
||||
|
||||
/// Utility macro to access an [`AccountConf`] setting field from
|
||||
/// [`Context`](crate::Context) indexed by `$account_hash` and a mailbox.
|
||||
///
|
||||
/// The value returned is the optionally overriden one in the
|
||||
/// [`FileMailboxConf::conf_override`] field, otherwise the
|
||||
/// [`AccountConf::conf_override`] field, otherwise the global one.
|
||||
///
|
||||
/// See also the [`account_settings`] macro.
|
||||
#[macro_export]
|
||||
macro_rules! mailbox_settings {
|
||||
($context:ident[$account_hash:expr][$mailbox_path:expr].$setting:ident.$field:ident) => {{
|
||||
$context.accounts[&$account_hash][$mailbox_path]
|
||||
.conf
|
||||
.conf_override
|
||||
.$setting
|
||||
.$field
|
||||
.as_ref()
|
||||
.or($context.accounts[&$account_hash]
|
||||
.settings
|
||||
.conf_override
|
||||
.$setting
|
||||
.$field
|
||||
.as_ref())
|
||||
.unwrap_or(&$context.settings.$setting.$field)
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MailUIConf {
|
||||
pub send_mail: Option<SendMail>,
|
||||
#[serde(default)]
|
||||
pub pager: PagerSettingsOverride,
|
||||
#[serde(default)]
|
||||
pub listing: ListingSettingsOverride,
|
||||
#[serde(default)]
|
||||
pub notifications: NotificationsSettingsOverride,
|
||||
#[serde(default)]
|
||||
pub shortcuts: ShortcutsOverride,
|
||||
#[serde(default)]
|
||||
pub composing: ComposingSettingsOverride,
|
||||
#[serde(default)]
|
||||
pub identity: Option<String>,
|
||||
#[serde(default)]
|
||||
pub tags: TagsSettingsOverride,
|
||||
#[serde(default)]
|
||||
pub themes: Option<Themes>,
|
||||
#[serde(default)]
|
||||
pub pgp: PGPSettingsOverride,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct FileMailboxConf {
|
||||
#[serde(flatten)]
|
||||
pub conf_override: MailUIConf,
|
||||
#[serde(default = "false_val")]
|
||||
pub collapsed: bool,
|
||||
#[serde(flatten)]
|
||||
pub mailbox_conf: MailboxConf,
|
||||
}
|
||||
|
||||
impl FileMailboxConf {
|
||||
pub fn conf_override(&self) -> &MailUIConf {
|
||||
&self.conf_override
|
||||
}
|
||||
|
||||
pub fn mailbox_conf(&self) -> &MailboxConf {
|
||||
&self.mailbox_conf
|
||||
}
|
||||
}
|
||||
|
||||
use crate::conf::deserializers::extra_settings;
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct FileAccount {
|
||||
pub root_mailbox: String,
|
||||
/// The mailbox that is the default to open / view for this account. Must be
|
||||
/// a valid mailbox path.
|
||||
///
|
||||
/// If not specified, the default is [`Self::root_mailbox`].
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub default_mailbox: Option<String>,
|
||||
pub format: String,
|
||||
pub send_mail: SendMail,
|
||||
pub identity: String,
|
||||
#[serde(default)]
|
||||
pub extra_identities: Vec<String>,
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub display_name: Option<String>,
|
||||
#[serde(default = "false_val")]
|
||||
pub read_only: bool,
|
||||
#[serde(default)]
|
||||
pub subscribed_mailboxes: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub mailboxes: IndexMap<String, FileMailboxConf>,
|
||||
#[serde(default)]
|
||||
pub search_backend: data_types::SearchBackend,
|
||||
#[serde(default)]
|
||||
pub order: (SortField, SortOrder),
|
||||
#[serde(default = "false_val")]
|
||||
pub manual_refresh: bool,
|
||||
#[serde(default = "none", skip_serializing_if = "Option::is_none")]
|
||||
pub refresh_command: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub conf_override: MailUIConf,
|
||||
#[serde(flatten)]
|
||||
#[serde(
|
||||
deserialize_with = "extra_settings",
|
||||
skip_serializing_if = "IndexMap::is_empty"
|
||||
)]
|
||||
/// Use custom deserializer to convert any given value (eg `bool`, number,
|
||||
/// etc) to `String`.
|
||||
pub extra: IndexMap<String, String>,
|
||||
}
|
||||
|
||||
impl FileAccount {
|
||||
pub fn mailboxes(&self) -> &IndexMap<String, FileMailboxConf> {
|
||||
&self.mailboxes
|
||||
}
|
||||
|
||||
pub fn search_backend(&self) -> &data_types::SearchBackend {
|
||||
&self.search_backend
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FileSettings {
|
||||
pub accounts: IndexMap<String, FileAccount>,
|
||||
#[serde(default)]
|
||||
pub pager: pager::PagerSettings,
|
||||
#[serde(default)]
|
||||
pub listing: listing::ListingSettings,
|
||||
#[serde(default)]
|
||||
pub notifications: notifications::NotificationsSettings,
|
||||
#[serde(default)]
|
||||
pub shortcuts: shortcuts::Shortcuts,
|
||||
#[serde(default)]
|
||||
pub composing: composing::ComposingSettings,
|
||||
#[serde(default)]
|
||||
pub tags: tags::TagsSettings,
|
||||
#[serde(default)]
|
||||
pub pgp: pgp::PGPSettings,
|
||||
#[serde(default)]
|
||||
pub terminal: terminal::TerminalSettings,
|
||||
#[serde(default)]
|
||||
pub log: LogSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct AccountConf {
|
||||
pub account: melib::AccountSettings,
|
||||
/// How to send e-mail for this account.
|
||||
/// Required
|
||||
pub send_mail: SendMail,
|
||||
pub default_mailbox: Option<MailboxHash>,
|
||||
pub sent_mailbox: Option<MailboxHash>,
|
||||
pub conf: FileAccount,
|
||||
pub conf_override: MailUIConf,
|
||||
pub mailbox_confs: IndexMap<String, FileMailboxConf>,
|
||||
}
|
||||
|
||||
impl AccountConf {
|
||||
pub fn account(&self) -> &melib::AccountSettings {
|
||||
&self.account
|
||||
}
|
||||
pub fn account_mut(&mut self) -> &mut melib::AccountSettings {
|
||||
&mut self.account
|
||||
}
|
||||
pub fn conf(&self) -> &FileAccount {
|
||||
&self.conf
|
||||
}
|
||||
pub fn conf_mut(&mut self) -> &mut FileAccount {
|
||||
&mut self.conf
|
||||
}
|
||||
}
|
||||
|
||||
impl From<melib::AccountSettings> for AccountConf {
|
||||
fn from(account: melib::AccountSettings) -> Self {
|
||||
Self {
|
||||
account,
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<FileAccount> for AccountConf {
|
||||
fn from(x: FileAccount) -> Self {
|
||||
let format = x.format.to_lowercase();
|
||||
let root_mailbox = x.root_mailbox.clone();
|
||||
let identity = x.identity.clone();
|
||||
let display_name = x.display_name.clone();
|
||||
let order = x.order;
|
||||
let mailboxes = x
|
||||
.mailboxes
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.mailbox_conf.clone()))
|
||||
.collect();
|
||||
|
||||
let account = melib::AccountSettings {
|
||||
name: String::new(),
|
||||
root_mailbox,
|
||||
format,
|
||||
identity,
|
||||
extra_identities: x.extra_identities.clone(),
|
||||
read_only: x.read_only,
|
||||
display_name,
|
||||
order,
|
||||
subscribed_mailboxes: x.subscribed_mailboxes.clone(),
|
||||
mailboxes,
|
||||
manual_refresh: x.manual_refresh,
|
||||
extra: x.extra.clone().into_iter().collect(),
|
||||
};
|
||||
|
||||
let mailbox_confs = x.mailboxes.clone();
|
||||
Self {
|
||||
send_mail: x.send_mail.clone(),
|
||||
default_mailbox: None,
|
||||
sent_mailbox: None,
|
||||
conf_override: x.conf_override.clone(),
|
||||
conf: x,
|
||||
mailbox_confs,
|
||||
..Self::from(account)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config_file() -> Result<PathBuf> {
|
||||
if let Ok(path) = env::var("MELI_CONFIG") {
|
||||
return Ok(PathBuf::from(path).expand());
|
||||
}
|
||||
let xdg_dirs = xdg::BaseDirectories::with_prefix("meli")?;
|
||||
xdg_dirs
|
||||
.place_config_file("config.toml")
|
||||
.chain_err_summary(|| {
|
||||
format!(
|
||||
"Cannot create configuration directory in {}",
|
||||
xdg_dirs.get_config_home().display()
|
||||
)
|
||||
})
|
||||
.chain_err_kind(ErrorKind::Platform)
|
||||
}
|
||||
|
||||
impl FileSettings {
|
||||
pub const EXAMPLE_CONFIG: &'static str = include_str!("../docs/samples/sample-config.toml");
|
||||
|
||||
pub fn new() -> Result<Self> {
|
||||
let config_path = get_config_file()?;
|
||||
if !config_path.exists() {
|
||||
let path_string = config_path.display().to_string();
|
||||
if path_string.is_empty() {
|
||||
return Err(Error::new("Given configuration path is empty.")
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
let ask = crate::terminal::Ask::new(format!(
|
||||
"No configuration found. Would you like to generate one in {}?",
|
||||
path_string
|
||||
));
|
||||
#[cfg(not(test))]
|
||||
let mut stdout = std::io::stdout();
|
||||
#[cfg(not(test))]
|
||||
let stdin = std::io::stdin();
|
||||
#[cfg(not(test))]
|
||||
if ask.run(&mut stdout, &mut stdin.lock()) {
|
||||
create_config_file(&config_path)?;
|
||||
return Err(
|
||||
Error::new("Edit the sample configuration and relaunch meli.")
|
||||
.set_kind(ErrorKind::Configuration),
|
||||
);
|
||||
}
|
||||
#[cfg(test)]
|
||||
return Ok(Self::default());
|
||||
#[cfg(not(test))]
|
||||
return Err(
|
||||
Error::new("No configuration file found.").set_kind(ErrorKind::Configuration)
|
||||
);
|
||||
}
|
||||
|
||||
let mut stdout = std::io::stdout();
|
||||
let stdin = std::io::stdin();
|
||||
crate::version_migrations::version_setup(&config_path, &mut stdout, &mut stdin.lock())?;
|
||||
Self::validate(config_path, false)
|
||||
}
|
||||
|
||||
/// Validate configuration from `input` string.
|
||||
pub fn validate_string(s: String, clear_extras: bool) -> Result<Self> {
|
||||
let _: toml::value::Table = melib::serde_path_to_error::deserialize(
|
||||
toml::Deserializer::new(&s),
|
||||
)
|
||||
.map_err(|err| {
|
||||
Error::new("Config file is invalid TOML")
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::ValueError)
|
||||
})?;
|
||||
|
||||
let mut s: Self = melib::serde_path_to_error::deserialize(toml::Deserializer::new(&s))
|
||||
.map_err(|err| {
|
||||
Error::new("Input contains errors")
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Configuration)
|
||||
})?;
|
||||
let backends = melib::backends::Backends::new();
|
||||
let Themes {
|
||||
light: default_light,
|
||||
dark: default_dark,
|
||||
..
|
||||
} = Themes::default();
|
||||
for (k, v) in default_light.keys.into_iter() {
|
||||
if !s.terminal.themes.light.contains_key(&k) {
|
||||
s.terminal.themes.light.insert(k, v);
|
||||
}
|
||||
}
|
||||
for theme in s.terminal.themes.other_themes.values_mut() {
|
||||
for (k, v) in default_dark.keys.clone().into_iter() {
|
||||
if !theme.contains_key(&k) {
|
||||
theme.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, v) in default_dark.keys.into_iter() {
|
||||
if !s.terminal.themes.dark.contains_key(&k) {
|
||||
s.terminal.themes.dark.insert(k, v);
|
||||
}
|
||||
}
|
||||
match s.terminal.theme.as_str() {
|
||||
themes::DARK | themes::LIGHT => {}
|
||||
t if s.terminal.themes.other_themes.contains_key(t) => {}
|
||||
t => {
|
||||
return Err(Error::new(format!("Theme `{}` was not found.", t))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
|
||||
s.terminal.themes.validate()?;
|
||||
for (name, acc) in s.accounts.iter_mut() {
|
||||
let FileAccount {
|
||||
root_mailbox,
|
||||
format,
|
||||
send_mail: _,
|
||||
identity,
|
||||
extra_identities,
|
||||
read_only,
|
||||
display_name,
|
||||
order,
|
||||
subscribed_mailboxes,
|
||||
mailboxes,
|
||||
extra,
|
||||
manual_refresh,
|
||||
default_mailbox: _,
|
||||
refresh_command: _,
|
||||
search_backend: _,
|
||||
conf_override: _,
|
||||
} = acc.clone();
|
||||
|
||||
let lowercase_format = format.to_lowercase();
|
||||
let mut s = melib::AccountSettings {
|
||||
name: name.to_string(),
|
||||
root_mailbox,
|
||||
format: format.clone(),
|
||||
identity,
|
||||
extra_identities,
|
||||
read_only,
|
||||
display_name,
|
||||
order,
|
||||
subscribed_mailboxes,
|
||||
manual_refresh,
|
||||
mailboxes: mailboxes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.mailbox_conf))
|
||||
.collect(),
|
||||
extra: extra.into_iter().collect(),
|
||||
};
|
||||
s.validate_config()?;
|
||||
backends.validate_config(&lowercase_format, &mut s)?;
|
||||
if !s.extra.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Unrecognised configuration values: {:?}",
|
||||
s.extra
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
if clear_extras {
|
||||
acc.extra.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Validate `path` and print errors.
|
||||
pub fn validate(path: PathBuf, clear_extras: bool) -> Result<Self> {
|
||||
let s = pp::pp(&path)?;
|
||||
let _: toml::value::Table = toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!(
|
||||
"{}: Config file is invalid TOML; {}",
|
||||
path.display(),
|
||||
err
|
||||
))
|
||||
})?;
|
||||
|
||||
let mut s: Self = toml::from_str(&s).map_err(|err| {
|
||||
Error::new(format!("{}: Config file contains errors", path.display()))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::Configuration)
|
||||
})?;
|
||||
let backends = melib::backends::Backends::new();
|
||||
let Themes {
|
||||
light: default_light,
|
||||
dark: default_dark,
|
||||
..
|
||||
} = Themes::default();
|
||||
for (k, v) in default_light.keys.into_iter() {
|
||||
if !s.terminal.themes.light.contains_key(&k) {
|
||||
s.terminal.themes.light.insert(k, v);
|
||||
}
|
||||
}
|
||||
for theme in s.terminal.themes.other_themes.values_mut() {
|
||||
for (k, v) in default_dark.keys.clone().into_iter() {
|
||||
if !theme.contains_key(&k) {
|
||||
theme.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (k, v) in default_dark.keys.into_iter() {
|
||||
if !s.terminal.themes.dark.contains_key(&k) {
|
||||
s.terminal.themes.dark.insert(k, v);
|
||||
}
|
||||
}
|
||||
match s.terminal.theme.as_str() {
|
||||
themes::DARK | themes::LIGHT => {}
|
||||
t if s.terminal.themes.other_themes.contains_key(t) => {}
|
||||
t => {
|
||||
return Err(Error::new(format!("Theme `{}` was not found.", t))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
}
|
||||
|
||||
s.terminal.themes.validate()?;
|
||||
for (name, acc) in s.accounts.iter_mut() {
|
||||
let FileAccount {
|
||||
root_mailbox,
|
||||
format,
|
||||
send_mail: _,
|
||||
identity,
|
||||
extra_identities,
|
||||
read_only,
|
||||
display_name,
|
||||
order,
|
||||
subscribed_mailboxes,
|
||||
mailboxes,
|
||||
extra,
|
||||
manual_refresh,
|
||||
default_mailbox: _,
|
||||
refresh_command: _,
|
||||
search_backend: _,
|
||||
conf_override: _,
|
||||
} = acc.clone();
|
||||
|
||||
let lowercase_format = format.to_lowercase();
|
||||
let mut s = melib::AccountSettings {
|
||||
name: name.to_string(),
|
||||
root_mailbox,
|
||||
format: format.clone(),
|
||||
identity,
|
||||
extra_identities,
|
||||
read_only,
|
||||
display_name,
|
||||
order,
|
||||
subscribed_mailboxes,
|
||||
manual_refresh,
|
||||
mailboxes: mailboxes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k, v.mailbox_conf))
|
||||
.collect(),
|
||||
extra: extra.into_iter().collect(),
|
||||
};
|
||||
s.validate_config()?;
|
||||
backends.validate_config(&lowercase_format, &mut s)?;
|
||||
if !s.extra.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Unrecognised configuration values: {:?}",
|
||||
s.extra
|
||||
))
|
||||
.set_kind(ErrorKind::Configuration));
|
||||
}
|
||||
if clear_extras {
|
||||
acc.extra.clear();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
pub struct Settings {
|
||||
pub accounts: IndexMap<String, AccountConf>,
|
||||
pub pager: pager::PagerSettings,
|
||||
pub listing: listing::ListingSettings,
|
||||
pub notifications: notifications::NotificationsSettings,
|
||||
pub shortcuts: shortcuts::Shortcuts,
|
||||
pub tags: tags::TagsSettings,
|
||||
pub composing: composing::ComposingSettings,
|
||||
pub pgp: pgp::PGPSettings,
|
||||
pub terminal: terminal::TerminalSettings,
|
||||
pub log: LogSettings,
|
||||
#[serde(skip)]
|
||||
pub _logger: StderrLogger,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new() -> Result<Self> {
|
||||
let fs = FileSettings::new()?;
|
||||
let mut s: IndexMap<String, AccountConf> = IndexMap::new();
|
||||
|
||||
for (id, x) in fs.accounts {
|
||||
let mut ac = AccountConf::from(x);
|
||||
ac.account.name.clone_from(&id);
|
||||
|
||||
s.insert(id, ac);
|
||||
}
|
||||
|
||||
let mut _logger = StderrLogger::new(fs.log.maximum_level);
|
||||
|
||||
if let Some(ref log_path) = fs.log.log_file {
|
||||
_logger.change_log_dest(log_path.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
accounts: s,
|
||||
pager: fs.pager,
|
||||
listing: fs.listing,
|
||||
notifications: fs.notifications,
|
||||
shortcuts: fs.shortcuts,
|
||||
tags: fs.tags,
|
||||
composing: fs.composing,
|
||||
pgp: fs.pgp,
|
||||
terminal: fs.terminal,
|
||||
log: fs.log,
|
||||
_logger,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn without_accounts() -> Result<Self> {
|
||||
let fs = FileSettings::new()?;
|
||||
let mut _logger = StderrLogger::new(fs.log.maximum_level);
|
||||
|
||||
if let Some(ref log_path) = fs.log.log_file {
|
||||
_logger.change_log_dest(log_path.into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
accounts: IndexMap::new(),
|
||||
pager: fs.pager,
|
||||
listing: fs.listing,
|
||||
notifications: fs.notifications,
|
||||
shortcuts: fs.shortcuts,
|
||||
tags: fs.tags,
|
||||
composing: fs.composing,
|
||||
pgp: fs.pgp,
|
||||
terminal: fs.terminal,
|
||||
log: fs.log,
|
||||
_logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
mod deserializers {
|
||||
use serde::{de, Deserialize, Deserializer};
|
||||
|
||||
pub(in crate::conf) fn non_empty_opt_string<'de, D, T: std::convert::From<Option<String>>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
if s.is_empty() {
|
||||
Ok(None.into())
|
||||
} else {
|
||||
Ok(Some(s).into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(in crate::conf) fn non_empty_string<'de, D, T: std::convert::From<String>>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<T, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
if s.is_empty() {
|
||||
Err(de::Error::custom("This field value cannot be empty."))
|
||||
} else {
|
||||
Ok(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
use toml::Value;
|
||||
fn any_of<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v: Value = Deserialize::deserialize(deserializer)?;
|
||||
if let Some(s) = v.as_str() {
|
||||
return Ok(s.to_string());
|
||||
}
|
||||
let mut ret = v.to_string();
|
||||
if (ret.starts_with('"') && ret.ends_with('"'))
|
||||
|| (ret.starts_with('\"') && ret.ends_with('\''))
|
||||
{
|
||||
ret.drain(0..1).count();
|
||||
ret.drain(ret.len() - 1..).count();
|
||||
}
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
use indexmap::IndexMap;
|
||||
pub(in crate::conf) fn extra_settings<'de, D>(
|
||||
deserializer: D,
|
||||
) -> std::result::Result<IndexMap<String, String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
/* Why is this needed? If the user gives a configuration value such as key =
|
||||
* true, the parsing will fail since it expects string values. We
|
||||
* want to accept key = true as well as key = "true". */
|
||||
#[derive(Deserialize)]
|
||||
struct Wrapper(#[serde(deserialize_with = "any_of")] String);
|
||||
|
||||
let v = <IndexMap<String, Wrapper>>::deserialize(deserializer)?;
|
||||
Ok(v.into_iter().map(|(k, Wrapper(v))| (k, v)).collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_config_file(p: &Path) -> Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(p)
|
||||
.chain_err_summary(|| format!("Cannot create configuration file in {}", p.display()))?;
|
||||
file.write_all(FileSettings::EXAMPLE_CONFIG.as_bytes())
|
||||
.and_then(|()| file.flush())
|
||||
.chain_err_summary(|| format!("Could not write to configuration file {}", p.display()))?;
|
||||
println!("Written example configuration to {}", p.display());
|
||||
let set_permissions = |file: std::fs::File| -> Result<()> {
|
||||
let metadata = file.metadata()?;
|
||||
let mut permissions = metadata.permissions();
|
||||
|
||||
permissions.set_mode(0o600); // Read/write for owner only.
|
||||
file.set_permissions(permissions)?;
|
||||
Ok(())
|
||||
};
|
||||
if let Err(err) = set_permissions(file) {
|
||||
println!(
|
||||
"Warning: Could not set permissions of {} to 0o600: {}",
|
||||
p.display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct LogSettings {
|
||||
#[serde(default)]
|
||||
pub log_file: Option<PathBuf>,
|
||||
#[serde(default)]
|
||||
pub maximum_level: melib::LogLevel,
|
||||
}
|
||||
|
||||
pub use data_types::dotaddressable::*;
|
|
@ -1,294 +0,0 @@
|
|||
//
|
||||
// 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()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,163 +0,0 @@
|
|||
//
|
||||
// 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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// 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))
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
//
|
||||
// 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()
|
||||
}
|
|
@ -1,281 +0,0 @@
|
|||
//
|
||||
// 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)
|
||||
}
|
|
@ -1,384 +0,0 @@
|
|||
//
|
||||
// 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,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2023 - 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/>.
|
||||
*/
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
168
meli/src/lib.rs
|
@ -1,168 +0,0 @@
|
|||
/*
|
||||
* meli - lib.rs
|
||||
*
|
||||
* Copyright 2017-2022 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/>.
|
||||
*/
|
||||
|
||||
#![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
|
||||
//! thread.
|
||||
//!
|
||||
//! The mail handling stuff is done in the `melib` crate which includes all
|
||||
//! backend needs. The split is done to theoretically be able to create
|
||||
//! different frontends with the same innards.
|
||||
|
||||
use std::alloc::System;
|
||||
pub use std::{collections::VecDeque, path::PathBuf};
|
||||
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
extern crate linkify;
|
||||
pub use melib::uuid;
|
||||
|
||||
pub extern crate bitflags;
|
||||
pub extern crate serde_json;
|
||||
pub extern crate smallvec;
|
||||
pub extern crate termion;
|
||||
|
||||
pub use structopt::StructOpt;
|
||||
|
||||
#[global_allocator]
|
||||
static GLOBAL: System = System;
|
||||
|
||||
pub extern crate melib;
|
||||
pub use melib::{
|
||||
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]
|
||||
pub mod types;
|
||||
pub use crate::types::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod terminal;
|
||||
pub use crate::terminal::*;
|
||||
|
||||
#[macro_use]
|
||||
pub mod command;
|
||||
pub use crate::command::*;
|
||||
|
||||
pub mod state;
|
||||
pub use crate::state::*;
|
||||
|
||||
pub mod components;
|
||||
pub use crate::components::*;
|
||||
|
||||
pub mod utilities;
|
||||
pub use crate::utilities::*;
|
||||
|
||||
pub mod contacts;
|
||||
pub use crate::contacts::*;
|
||||
|
||||
pub mod mail;
|
||||
pub use crate::mail::*;
|
||||
|
||||
pub mod notifications;
|
||||
|
||||
pub mod manage;
|
||||
pub use manage::*;
|
||||
|
||||
// #[cfg(feature = "svgscreenshot")]
|
||||
// pub mod svg;
|
||||
|
||||
#[macro_use]
|
||||
pub mod conf;
|
||||
pub use crate::conf::{
|
||||
data_types::{IndexStyle, SearchBackend},
|
||||
DotAddressable, Settings, Shortcuts, ThemeAttribute,
|
||||
};
|
||||
|
||||
#[cfg(feature = "sqlite3")]
|
||||
pub mod sqlite3;
|
||||
|
||||
pub mod jobs;
|
||||
pub mod mailcap;
|
||||
|
||||
pub mod accounts;
|
||||
pub use self::accounts::Account;
|
||||
|
||||
pub mod patch_retrieve;
|
||||
|
||||
pub mod version_migrations;
|
|
@ -1,574 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2020 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/>.
|
||||
*/
|
||||
|
||||
use super::*;
|
||||
|
||||
type KeylistJoinHandle = JoinHandle<Result<Vec<melib::gpgme::Key>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
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,
|
||||
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,
|
||||
err: Error,
|
||||
},
|
||||
Loaded {
|
||||
widget: Box<UIDialog<melib::gpgme::Key>>,
|
||||
keys: Vec<melib::gpgme::Key>,
|
||||
},
|
||||
}
|
||||
|
||||
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 Component for KeySelection {
|
||||
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.draw(grid, area.center_inside((2, 2)), context),
|
||||
Self::Error { ref err, .. } => {
|
||||
let theme_default = crate::conf::value(context, "theme_default");
|
||||
grid.write_string(
|
||||
&err.to_string(),
|
||||
theme_default.fg,
|
||||
theme_default.bg,
|
||||
theme_default.attrs,
|
||||
area.center_inside((15, 2)),
|
||||
None,
|
||||
Some(0),
|
||||
);
|
||||
}
|
||||
Self::Loaded { ref mut widget, .. } => widget.draw(grid, area, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
match self {
|
||||
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 == 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 */ }
|
||||
Ok(Some(Ok(keys))) => {
|
||||
if keys.is_empty() {
|
||||
let id = progress_spinner.id();
|
||||
if allow_remote_lookup.is_true() {
|
||||
match KeySelectionLoading::new(
|
||||
*secret,
|
||||
*local,
|
||||
(std::mem::take(pattern), std::mem::take(other_patterns)),
|
||||
*allow_remote_lookup,
|
||||
context,
|
||||
) {
|
||||
Ok(inner) => {
|
||||
let keys_accumulator = std::mem::take(keys_accumulator);
|
||||
*self = Self::Loading {
|
||||
inner,
|
||||
keys_accumulator,
|
||||
};
|
||||
}
|
||||
Err(err) => *self = Self::Error { err, id },
|
||||
}
|
||||
} else if !*local && allow_remote_lookup.is_ask() {
|
||||
*self = Self::Error {
|
||||
err: Error::new(format!(
|
||||
"No keys found for {}, perform remote lookup?",
|
||||
pattern
|
||||
)),
|
||||
id,
|
||||
}
|
||||
} else {
|
||||
*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 Self::Error { ref err, .. } = self {
|
||||
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(id, Box::new(res)));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
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))) => {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => progress_spinner.process_event(event, context),
|
||||
},
|
||||
Self::Error { .. } => false,
|
||||
Self::Loaded { ref mut widget, .. } => widget.process_event(event, context),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.is_dirty(),
|
||||
Self::Error { .. } => true,
|
||||
Self::Loaded { ref widget, .. } => widget.is_dirty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, value: bool) {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref mut progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.set_dirty(value),
|
||||
Self::Error { .. } => {}
|
||||
Self::Loaded { ref mut widget, .. } => widget.set_dirty(value),
|
||||
}
|
||||
}
|
||||
|
||||
fn kill(&mut self, _uuid: ComponentId, _context: &mut Context) {}
|
||||
|
||||
fn shortcuts(&self, context: &Context) -> ShortcutMaps {
|
||||
match self {
|
||||
Self::Loading { .. } | Self::Error { .. } => ShortcutMaps::default(),
|
||||
Self::Loaded { ref widget, .. } => widget.shortcuts(context),
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
match self {
|
||||
Self::Loading {
|
||||
inner:
|
||||
KeySelectionLoading {
|
||||
ref progress_spinner,
|
||||
..
|
||||
},
|
||||
keys_accumulator: _,
|
||||
} => progress_spinner.id(),
|
||||
Self::Error { ref id, .. } => *id,
|
||||
Self::Loaded { ref widget, .. } => widget.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpgComposeState {
|
||||
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>,
|
||||
}
|
||||
|
||||
impl Default for GpgComposeState {
|
||||
fn default() -> Self {
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,263 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2019 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/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
collections::{hash_map::DefaultHasher, BTreeMap},
|
||||
future::Future,
|
||||
hash::{Hash, Hasher},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use melib::{
|
||||
email::{
|
||||
attachment_types::{ContentDisposition, ContentType, MultipartType},
|
||||
pgp as melib_pgp, Attachment, AttachmentBuilder,
|
||||
},
|
||||
error::*,
|
||||
gpgme::*,
|
||||
parser::BytesExt,
|
||||
};
|
||||
|
||||
use super::AttachmentBoxFuture;
|
||||
|
||||
pub async fn decrypt(raw: Vec<u8>) -> Result<(melib_pgp::DecryptionMetadata, Vec<u8>)> {
|
||||
let mut ctx = Context::new()?;
|
||||
let cipher = ctx.new_data_mem(&raw)?;
|
||||
ctx.decrypt(cipher)?.await
|
||||
}
|
||||
|
||||
pub fn verify(a: Attachment) -> impl Future<Output = Result<()>> {
|
||||
thread_local! {
|
||||
static CACHE: Arc<Mutex<BTreeMap<u64, Result<()>>>> = Arc::new(Mutex::new(BTreeMap::new()));
|
||||
}
|
||||
|
||||
let hash_mtx = CACHE.with(|cache| cache.clone());
|
||||
verify_inner(a, hash_mtx)
|
||||
}
|
||||
|
||||
async fn verify_inner(a: Attachment, cache: Arc<Mutex<BTreeMap<u64, Result<()>>>>) -> Result<()> {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
a.hash(&mut hasher);
|
||||
let attachment_hash: u64 = hasher.finish();
|
||||
|
||||
{
|
||||
let lck = cache.lock().unwrap();
|
||||
let in_cache: bool = lck.contains_key(&attachment_hash);
|
||||
if in_cache {
|
||||
return lck[&attachment_hash].clone();
|
||||
}
|
||||
}
|
||||
|
||||
let (data, sig) =
|
||||
melib_pgp::verify_signature(&a).chain_err_summary(|| "Could not verify signature.")?;
|
||||
let mut ctx = Context::new()?;
|
||||
let sig = ctx.new_data_mem(sig.body().trim())?;
|
||||
let data = ctx.new_data_mem(&data)?;
|
||||
|
||||
let result = ctx.verify(sig, data)?.await;
|
||||
{
|
||||
let mut lck = cache.lock().unwrap();
|
||||
lck.insert(attachment_hash, result.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn sign_filter(
|
||||
default_key: Option<String>,
|
||||
mut sign_keys: Vec<Key>,
|
||||
) -> Result<impl FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send> {
|
||||
Ok(move |a: AttachmentBuilder| -> AttachmentBoxFuture {
|
||||
Box::pin(async move {
|
||||
if let Some(default_key) = default_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(false, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate sign key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
sign_keys.extend(keys);
|
||||
}
|
||||
if sign_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for signing; please select one.",
|
||||
));
|
||||
}
|
||||
let a: Attachment = a.into();
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(&melib_pgp::convert_attachment_to_rfc_spec(
|
||||
a.into_raw().as_bytes(),
|
||||
))?;
|
||||
let sig_attachment = Attachment::new(
|
||||
ContentType::PGPSignature,
|
||||
Default::default(),
|
||||
ctx.sign(sign_keys, data)?.await?,
|
||||
);
|
||||
let a: AttachmentBuilder = a.into();
|
||||
let parts = vec![a, sig_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Ok(Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Signed,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
.into())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn encrypt_filter(
|
||||
encrypt_for_self: Option<melib::Address>,
|
||||
default_sign_key: Option<String>,
|
||||
mut sign_keys: Option<Vec<Key>>,
|
||||
default_encrypt_key: Option<String>,
|
||||
mut encrypt_keys: Vec<Key>,
|
||||
) -> Result<impl FnOnce(AttachmentBuilder) -> AttachmentBoxFuture + Send> {
|
||||
Ok(move |a: AttachmentBuilder| -> AttachmentBoxFuture {
|
||||
Box::pin(async move {
|
||||
if let Some(default_key) = default_sign_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(true, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate sign key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
if let Some(ref mut sign_keys) = sign_keys {
|
||||
sign_keys.extend(keys);
|
||||
} else {
|
||||
sign_keys = Some(keys);
|
||||
}
|
||||
}
|
||||
if let Some(ref sign_keys) = sign_keys {
|
||||
if sign_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for signing; please select one.",
|
||||
));
|
||||
}
|
||||
}
|
||||
if let Some(default_key) = default_encrypt_key {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx.keylist(false, Some(default_key.clone()))?.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate encryption key with ID `{}`",
|
||||
default_key
|
||||
)));
|
||||
}
|
||||
encrypt_keys.extend(keys);
|
||||
}
|
||||
if encrypt_keys.is_empty() {
|
||||
return Err(Error::new(
|
||||
"No key was selected for encryption; please select one.",
|
||||
));
|
||||
}
|
||||
if let Some(encrypt_for_self) = encrypt_for_self {
|
||||
let mut ctx = Context::new()?;
|
||||
ctx.set_auto_key_locate(LocateKey::LOCAL)?;
|
||||
let keys = ctx
|
||||
.keylist(false, Some(encrypt_for_self.to_string()))?
|
||||
.await?;
|
||||
if keys.is_empty() {
|
||||
return Err(Error::new(format!(
|
||||
"Could not locate personal encryption key for address `{}`",
|
||||
encrypt_for_self
|
||||
)));
|
||||
}
|
||||
for key in keys {
|
||||
if !encrypt_keys.contains(&key) {
|
||||
encrypt_keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
let a: Attachment = if let Some(sign_keys) = sign_keys {
|
||||
let a: Attachment = a.into();
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(&melib_pgp::convert_attachment_to_rfc_spec(
|
||||
a.into_raw().as_bytes(),
|
||||
))?;
|
||||
let sig_attachment = Attachment::new(
|
||||
ContentType::PGPSignature,
|
||||
Default::default(),
|
||||
ctx.sign(sign_keys, data)?.await?,
|
||||
);
|
||||
let a: AttachmentBuilder = a.into();
|
||||
let parts = vec![a, sig_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Signed,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
} else {
|
||||
a.into()
|
||||
};
|
||||
let mut ctx = Context::new()?;
|
||||
let data = ctx.new_data_mem(a.into_raw().as_bytes())?;
|
||||
|
||||
let enc_attachment = {
|
||||
let mut a = Attachment::new(
|
||||
ContentType::OctetStream {
|
||||
name: None,
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
ctx.encrypt(encrypt_keys, data)?.await?,
|
||||
);
|
||||
a.content_disposition =
|
||||
ContentDisposition::from(br#"attachment; filename="msg.asc""#);
|
||||
a
|
||||
};
|
||||
let mut a: AttachmentBuilder = AttachmentBuilder::new(b"Version: 1\n");
|
||||
|
||||
a.set_content_type_from_bytes(b"application/pgp-encrypted");
|
||||
a.set_content_disposition(ContentDisposition::from(b"attachment"));
|
||||
let parts = vec![a, enc_attachment.into()];
|
||||
let boundary = ContentType::make_boundary(&parts);
|
||||
Ok(Attachment::new(
|
||||
ContentType::Multipart {
|
||||
boundary: boundary.into_bytes(),
|
||||
kind: MultipartType::Encrypted,
|
||||
parts: parts.into_iter().map(|a| a.into()).collect::<Vec<_>>(),
|
||||
parameters: vec![],
|
||||
},
|
||||
Default::default(),
|
||||
vec![],
|
||||
)
|
||||
.into())
|
||||
})
|
||||
})
|
||||
}
|
|
@ -1,882 +0,0 @@
|
|||
/*
|
||||
* meli
|
||||
*
|
||||
* Copyright 2023 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/>.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::Write,
|
||||
process::{Command, Stdio},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
type ProcessEventFn = fn(&mut ViewFilter, &mut UIEvent, &mut Context) -> bool;
|
||||
|
||||
use melib::{
|
||||
attachment_types::{ContentType, MultipartType, Text},
|
||||
error::*,
|
||||
log,
|
||||
parser::BytesExt,
|
||||
text::Truncate,
|
||||
utils::xdg::query_default_app,
|
||||
Attachment, AttachmentBuilder, Result,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
components::*,
|
||||
desktop_exec_to_command,
|
||||
jobs::{IsAsync, JobId, JoinHandle},
|
||||
mail::view::ViewSettings,
|
||||
terminal::{Area, CellBuffer},
|
||||
try_recv_timeout, Context, ErrorKind, File, StatusEvent, UIEvent,
|
||||
};
|
||||
|
||||
type FilterResult = std::result::Result<(Attachment, Vec<u8>), (Error, Vec<u8>)>;
|
||||
type OnSuccessNoticeCb = Arc<dyn (Fn() -> Cow<'static, str>) + Send + Sync>;
|
||||
|
||||
pub enum ViewFilterContent {
|
||||
Running {
|
||||
job_id: JobId,
|
||||
on_success_notice_cb: OnSuccessNoticeCb,
|
||||
job_handle: JoinHandle<FilterResult>,
|
||||
view_settings: ViewSettings,
|
||||
},
|
||||
Error {
|
||||
inner: Error,
|
||||
},
|
||||
Filtered {
|
||||
inner: String,
|
||||
},
|
||||
InlineAttachments {
|
||||
parts: Vec<ViewFilter>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ViewFilterContent {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
use ViewFilterContent::*;
|
||||
match self {
|
||||
Running {
|
||||
ref job_id,
|
||||
on_success_notice_cb: _,
|
||||
job_handle: _,
|
||||
view_settings: _,
|
||||
} => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Running))
|
||||
.field("job_id", &job_id)
|
||||
.finish(),
|
||||
Error { ref inner } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Error))
|
||||
.field("error", inner)
|
||||
.finish(),
|
||||
Filtered { ref inner } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::Filtered))
|
||||
.field("body_text", &inner.trim_at_boundary(18))
|
||||
.field("body_text_len", &inner.len())
|
||||
.finish(),
|
||||
InlineAttachments { ref parts } => fmt
|
||||
.debug_struct(stringify!(ViewFilterContent::InlineAttachments))
|
||||
.field("parts_no", &parts.len())
|
||||
.field("parts", &parts)
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ViewFilter {
|
||||
pub filter_invocation: String,
|
||||
pub content_type: ContentType,
|
||||
pub notice: Option<Cow<'static, str>>,
|
||||
pub body_text: ViewFilterContent,
|
||||
pub unfiltered: Vec<u8>,
|
||||
pub event_handler: Option<ProcessEventFn>,
|
||||
pub id: ComponentId,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ViewFilter {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
fmt.debug_struct(melib::identify!(ViewFilter))
|
||||
.field("filter_invocation", &self.filter_invocation)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("notice", &self.notice)
|
||||
.field("body_text", &self.body_text)
|
||||
.field("event_handler", &self.event_handler.is_some())
|
||||
.field("id", &self.id)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ViewFilter {
|
||||
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(fmt, "{}", self.filter_invocation.trim_at_boundary(5))
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewFilter {
|
||||
pub fn new_html(
|
||||
body: &Attachment,
|
||||
view_settings: &ViewSettings,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
fn run(cmd: &str, args: &[&str], bytes: &[u8]) -> Result<String> {
|
||||
let mut html_filter = Command::new(cmd)
|
||||
.args(args)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?;
|
||||
html_filter
|
||||
.stdin
|
||||
.as_mut()
|
||||
.ok_or("Failed to write to html filter stdin")?
|
||||
.write_all(bytes)
|
||||
.chain_err_summary(|| "Failed to write to html filter stdin")?;
|
||||
Ok(String::from_utf8_lossy(
|
||||
&html_filter
|
||||
.wait_with_output()
|
||||
.chain_err_summary(|| "Could not wait for process output")?
|
||||
.stdout,
|
||||
)
|
||||
.into())
|
||||
}
|
||||
|
||||
let mut att = body;
|
||||
let mut stack = vec![body];
|
||||
while let Some(a) = stack.pop() {
|
||||
match a.content_type {
|
||||
ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} => {
|
||||
att = a;
|
||||
break;
|
||||
}
|
||||
ContentType::Text { .. }
|
||||
| ContentType::PGPSignature
|
||||
| ContentType::CMSSignature => {
|
||||
continue;
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Related,
|
||||
ref parts,
|
||||
ref parameters,
|
||||
..
|
||||
} => {
|
||||
if let Some(main_attachment) = parameters
|
||||
.iter()
|
||||
.find_map(|(k, v)| if k == b"type" { Some(v) } else { None })
|
||||
.and_then(|t| parts.iter().find(|a| a.content_type == t.as_slice()))
|
||||
{
|
||||
stack.push(main_attachment);
|
||||
} else {
|
||||
for a in parts {
|
||||
if let ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} = a.content_type
|
||||
{
|
||||
att = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
stack.extend(parts);
|
||||
}
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
ref parts,
|
||||
..
|
||||
} => {
|
||||
for a in parts {
|
||||
if let ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} = a.content_type
|
||||
{
|
||||
att = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
stack.extend(parts);
|
||||
}
|
||||
ContentType::Multipart {
|
||||
kind: _, ref parts, ..
|
||||
} => {
|
||||
for a in parts {
|
||||
if let ContentType::Text {
|
||||
kind: Text::Html, ..
|
||||
} = a.content_type
|
||||
{
|
||||
att = a;
|
||||
break;
|
||||
}
|
||||
}
|
||||
stack.extend(parts);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let settings = &context.settings;
|
||||
let (filter_invocation, cmd, args): (
|
||||
Cow<'static, str>,
|
||||
&'static str,
|
||||
SmallVec<[Cow<'static, str>; 8]>,
|
||||
) = if let Some(filter_invocation) = settings.pager.html_filter.as_ref() {
|
||||
(
|
||||
filter_invocation.to_string().into(),
|
||||
"sh",
|
||||
smallvec::smallvec!["-c".into(), filter_invocation.to_string().into()],
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"w3m -I utf-8 -T text/html".into(),
|
||||
"w3m",
|
||||
smallvec::smallvec!["-I".into(), "utf-8".into(), "-T".into(), "text/html".into()],
|
||||
)
|
||||
};
|
||||
let bytes: Vec<u8> = att.decode(Default::default());
|
||||
|
||||
let filter_invocation2 = filter_invocation.to_string();
|
||||
let bytes2 = bytes.clone();
|
||||
let job = async move {
|
||||
let filter_invocation = filter_invocation2;
|
||||
let bytes = bytes2;
|
||||
let borrowed_args = args
|
||||
.iter()
|
||||
.map(|a| a.as_ref())
|
||||
.collect::<SmallVec<[&str; 8]>>();
|
||||
match run(cmd, &borrowed_args, &bytes) {
|
||||
Err(err) => Err((
|
||||
Error::new(format!(
|
||||
"Failed to start html filter process `{}`",
|
||||
filter_invocation,
|
||||
))
|
||||
.set_source(Some(Arc::new(err)))
|
||||
.set_kind(ErrorKind::External),
|
||||
bytes,
|
||||
)),
|
||||
Ok(body_text) => {
|
||||
let mut att = AttachmentBuilder::default();
|
||||
att.set_raw(body_text.into_bytes()).set_body_to_raw();
|
||||
Ok((att.build(), bytes))
|
||||
}
|
||||
}
|
||||
};
|
||||
let filter_invocation2 = filter_invocation.to_string();
|
||||
let open_html_shortcut = settings.shortcuts.envelope_view.open_html.clone();
|
||||
let on_success_notice_cb = Arc::new(move || {
|
||||
format!(
|
||||
"Text piped through `{}` Press `{}` to open in web browser.",
|
||||
filter_invocation2, open_html_shortcut
|
||||
)
|
||||
.into()
|
||||
});
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
filter_invocation.to_string().into(),
|
||||
job,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let mut retval = Self {
|
||||
filter_invocation: filter_invocation.to_string(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
unfiltered: bytes,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = Some(Self::html_process_event);
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_attachment(
|
||||
att: &Attachment,
|
||||
view_settings: &ViewSettings,
|
||||
context: &Context,
|
||||
) -> Result<Self> {
|
||||
if matches!(
|
||||
att.content_type,
|
||||
ContentType::Other { .. } | ContentType::OctetStream { .. }
|
||||
) {
|
||||
return Err(Error::new(format!(
|
||||
"Cannot view {} attachment as text.",
|
||||
att.content_type,
|
||||
))
|
||||
.set_kind(ErrorKind::ValueError));
|
||||
}
|
||||
if let ContentType::Multipart {
|
||||
kind: MultipartType::Alternative,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
if let Some(Ok(v)) = parts
|
||||
.iter()
|
||||
.find(|p| p.is_text() && !p.body().trim().is_empty())
|
||||
.map(|p| Self::new_attachment(p, view_settings, context))
|
||||
{
|
||||
return Ok(v);
|
||||
}
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Related | MultipartType::Mixed,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
return Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::InlineAttachments {
|
||||
parts: parts
|
||||
.iter()
|
||||
.filter_map(|p| Self::new_attachment(p, view_settings, context).ok())
|
||||
.collect::<Vec<Self>>(),
|
||||
},
|
||||
unfiltered: att.decode(Default::default()),
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
if att.is_html() {
|
||||
return Self::new_html(att, view_settings, context);
|
||||
}
|
||||
if matches!(
|
||||
att.content_type,
|
||||
ContentType::Multipart {
|
||||
kind: MultipartType::Digest,
|
||||
..
|
||||
}
|
||||
) {
|
||||
return Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: vec![],
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Signed,
|
||||
ref parts,
|
||||
ref boundary,
|
||||
ref parameters,
|
||||
} = att.content_type
|
||||
{
|
||||
if !view_settings.auto_verify_signatures.is_true() {
|
||||
let att = Attachment {
|
||||
content_type: ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
parts: parts.clone(),
|
||||
parameters: parameters.clone(),
|
||||
boundary: boundary.clone(),
|
||||
},
|
||||
..att.clone()
|
||||
};
|
||||
return Ok(Self {
|
||||
notice: Some("Unverified signature.".into()),
|
||||
..Self::new_attachment(&att, view_settings, context)?
|
||||
});
|
||||
}
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
{
|
||||
let content = att.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
return Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: Some(
|
||||
"Cannot verify signature: meli must be compiled with libgpgme support."
|
||||
.into(),
|
||||
),
|
||||
body_text: ViewFilterContent::InlineAttachments {
|
||||
parts: parts
|
||||
.iter()
|
||||
.filter_map(|p| Self::new_attachment(p, view_settings, context).ok())
|
||||
.collect::<Vec<Self>>(),
|
||||
},
|
||||
unfiltered: bytes,
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
for a in parts {
|
||||
if a.content_type == "application/pgp-signature" {
|
||||
let content = att.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
let verify_fut = {
|
||||
let a = Attachment {
|
||||
content_type: ContentType::Multipart {
|
||||
kind: MultipartType::Mixed,
|
||||
parts: parts.clone(),
|
||||
parameters: parameters.clone(),
|
||||
boundary: boundary.clone(),
|
||||
},
|
||||
..att.clone()
|
||||
};
|
||||
let att = att.clone();
|
||||
async move {
|
||||
crate::mail::pgp::verify(att)
|
||||
.await
|
||||
.map_err(|err| (err, bytes.clone()))
|
||||
.map(|_| (a, bytes))
|
||||
}
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::verify".into(),
|
||||
verify_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Verified signature.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::verify".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: att.raw().to_vec(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let ContentType::Multipart {
|
||||
kind: MultipartType::Encrypted,
|
||||
ref parts,
|
||||
..
|
||||
} = att.content_type
|
||||
{
|
||||
#[cfg(not(feature = "gpgme"))]
|
||||
{
|
||||
let msg = "Cannot decrypt: meli must be compiled with libgpgme support.";
|
||||
if let Some(Ok(mut res)) =
|
||||
parts.iter().find_map(|part| {
|
||||
match Self::new_attachment(part, view_settings, context) {
|
||||
v @ Ok(_) => Some(v),
|
||||
Err(_) => None,
|
||||
}
|
||||
})
|
||||
{
|
||||
match res.notice {
|
||||
Some(ref mut notice) => {
|
||||
let notice = std::mem::take(notice);
|
||||
let mut notice = notice.into_owned();
|
||||
notice.push_str("\n");
|
||||
notice.push_str(msg);
|
||||
|
||||
res.notice = Some(notice.into());
|
||||
}
|
||||
None => {
|
||||
res.notice = Some(msg.into());
|
||||
}
|
||||
}
|
||||
return Ok(res);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
{
|
||||
for a in parts {
|
||||
if a.content_type == "application/octet-stream" {
|
||||
let content = a.raw();
|
||||
let bytes = content.trim().to_vec();
|
||||
let decrypt_fut = async {
|
||||
let (_metadata, bytes) = crate::mail::pgp::decrypt(
|
||||
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| (err, bytes))?;
|
||||
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::decrypt".into(),
|
||||
decrypt_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Decrypted content.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::decrypt".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: a.raw().to_vec(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "gpgme")]
|
||||
if let ContentType::Text {
|
||||
kind: Text::Plain, ..
|
||||
} = att.content_type
|
||||
{
|
||||
let content = att.text(Text::Plain);
|
||||
if content
|
||||
.trim_start()
|
||||
.starts_with("-----BEGIN PGP MESSAGE-----")
|
||||
&& content.trim_end().ends_with("-----END PGP MESSAGE-----")
|
||||
{
|
||||
let bytes = content.trim().to_string().into_bytes();
|
||||
let decrypt_fut = async {
|
||||
let (_metadata, bytes) = crate::mail::pgp::decrypt(
|
||||
melib::email::pgp::convert_attachment_to_rfc_spec(&bytes),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| (err, bytes))?;
|
||||
Ok((AttachmentBuilder::new(&bytes).build(), bytes))
|
||||
};
|
||||
let mut job_handle = context.main_loop_handler.job_executor.spawn(
|
||||
"gpg::decrypt".into(),
|
||||
decrypt_fut,
|
||||
IsAsync::Blocking,
|
||||
);
|
||||
let on_success_notice_cb = Arc::new(|| "Decrypted content.".into());
|
||||
let mut retval = Self {
|
||||
filter_invocation: "gpg::decrypt".into(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
unfiltered: content.into_bytes(),
|
||||
event_handler: Some(Self::job_process_event),
|
||||
id: ComponentId::default(),
|
||||
};
|
||||
if let Ok(Some(job_result)) = try_recv_timeout!(&mut job_handle.chan) {
|
||||
retval.body_text = ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb: on_success_notice_cb.clone(),
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
};
|
||||
retval.event_handler = None;
|
||||
Self::process_job_result(
|
||||
&mut retval,
|
||||
Ok(Some(job_result)),
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
context,
|
||||
);
|
||||
return Ok(retval);
|
||||
}
|
||||
return Ok(Self {
|
||||
body_text: ViewFilterContent::Running {
|
||||
job_id: job_handle.job_id,
|
||||
on_success_notice_cb,
|
||||
job_handle,
|
||||
view_settings: view_settings.clone(),
|
||||
},
|
||||
..retval
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(Self {
|
||||
filter_invocation: String::new(),
|
||||
content_type: att.content_type.clone(),
|
||||
notice: None,
|
||||
body_text: ViewFilterContent::Filtered {
|
||||
inner: att.text(Text::Plain),
|
||||
},
|
||||
unfiltered: att.decode(Default::default()),
|
||||
event_handler: None,
|
||||
id: ComponentId::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn html_process_event(self_: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if matches!(event, UIEvent::Input(key) if *key == context.settings.shortcuts.envelope_view.open_html)
|
||||
{
|
||||
let command = context
|
||||
.settings
|
||||
.pager
|
||||
.html_open
|
||||
.as_ref()
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| query_default_app("text/html").ok());
|
||||
let command = if cfg!(target_os = "macos") {
|
||||
command.or_else(|| Some("open".into()))
|
||||
} else if cfg!(target_os = "linux") {
|
||||
command.or_else(|| Some("xdg-open".into()))
|
||||
} else {
|
||||
command
|
||||
};
|
||||
if let Some(command) = command {
|
||||
let res = File::create_temp_file(&self_.unfiltered, None, None, Some("html"), true)
|
||||
.and_then(|p| {
|
||||
let exec_cmd = desktop_exec_to_command(
|
||||
&command,
|
||||
p.path().display().to_string(),
|
||||
false,
|
||||
);
|
||||
|
||||
Ok((
|
||||
p,
|
||||
Command::new("sh")
|
||||
.args(["-c", &exec_cmd])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.spawn()?,
|
||||
))
|
||||
});
|
||||
match res {
|
||||
Ok((p, child)) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::UpdateSubStatus(command.clone()),
|
||||
));
|
||||
context.temp_files.push(p);
|
||||
context
|
||||
.children
|
||||
.entry(command.into())
|
||||
.or_default()
|
||||
.push(child);
|
||||
}
|
||||
Err(err) => {
|
||||
context.replies.push_back(UIEvent::StatusEvent(
|
||||
StatusEvent::DisplayMessage(format!(
|
||||
"Failed to start `{command}`: {err}",
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context
|
||||
.replies
|
||||
.push_back(UIEvent::StatusEvent(StatusEvent::DisplayMessage(
|
||||
"Couldn't find a default application for html files.".to_string(),
|
||||
)));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn contains_job_id(&self, match_job_id: JobId) -> bool {
|
||||
if let ViewFilterContent::Running { ref job_id, .. } = self.body_text {
|
||||
return *job_id == match_job_id;
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref parts, .. } = self.body_text {
|
||||
return parts.iter().any(|p| p.contains_job_id(match_job_id));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn job_process_event(self_: &mut Self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
log::trace!(
|
||||
"job_process_event: self_ = {:?}, event = {:?}",
|
||||
self_,
|
||||
event
|
||||
);
|
||||
if matches!(event, UIEvent::StatusEvent(StatusEvent::JobFinished(ref job_id)) if self_.contains_job_id(*job_id))
|
||||
{
|
||||
if matches!(self_.body_text, ViewFilterContent::Running { .. }) {
|
||||
if let ViewFilterContent::Running {
|
||||
job_id: _,
|
||||
mut job_handle,
|
||||
on_success_notice_cb,
|
||||
view_settings,
|
||||
} = std::mem::replace(
|
||||
&mut self_.body_text,
|
||||
ViewFilterContent::Filtered {
|
||||
inner: String::new(),
|
||||
},
|
||||
) {
|
||||
log::trace!("job_process_event: inside if let ");
|
||||
let job_result = job_handle.chan.try_recv();
|
||||
Self::process_job_result(
|
||||
self_,
|
||||
job_result,
|
||||
on_success_notice_cb,
|
||||
&view_settings,
|
||||
context,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref mut parts, .. } = self_.body_text {
|
||||
return parts
|
||||
.iter_mut()
|
||||
.any(|p| Self::job_process_event(p, event, context));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn process_job_result(
|
||||
self_: &mut Self,
|
||||
result: std::result::Result<Option<FilterResult>, ::futures::channel::oneshot::Canceled>,
|
||||
on_success_notice_cb: OnSuccessNoticeCb,
|
||||
view_settings: &ViewSettings,
|
||||
context: &Context,
|
||||
) {
|
||||
match result {
|
||||
Err(err) => {
|
||||
self_.event_handler = None;
|
||||
/* Job was cancelled */
|
||||
self_.body_text = ViewFilterContent::Error {
|
||||
inner: Error::new("Job was cancelled.").set_source(Some(Arc::new(err))),
|
||||
};
|
||||
self_.notice = Some(format!("{} cancelled", self_.filter_invocation).into());
|
||||
}
|
||||
Ok(None) => {
|
||||
self_.event_handler = None;
|
||||
// something happened, perhaps a worker thread panicked
|
||||
self_.body_text = ViewFilterContent::Error {
|
||||
inner: Error::new(
|
||||
"Unknown error. Maybe some process panicked in the background?",
|
||||
),
|
||||
};
|
||||
self_.notice = Some(format!("{} failed", self_.filter_invocation).into());
|
||||
}
|
||||
Ok(Some(Ok((att, bytes)))) => {
|
||||
self_.event_handler = None;
|
||||
log::trace!("job_process_event: OK ");
|
||||
match Self::new_attachment(&att, view_settings, context) {
|
||||
Ok(mut new_self) => {
|
||||
if self_.content_type.is_text_html() {
|
||||
new_self.event_handler = Some(Self::html_process_event);
|
||||
}
|
||||
new_self.unfiltered = bytes;
|
||||
new_self.notice = Some(on_success_notice_cb());
|
||||
*self_ = new_self;
|
||||
}
|
||||
Err(err) => {
|
||||
self_.body_text = ViewFilterContent::Error { inner: err };
|
||||
self_.notice = Some(
|
||||
format!("decoding result of {} failed", self_.filter_invocation).into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(Err((error, bytes)))) => {
|
||||
self_.event_handler = None;
|
||||
self_.body_text = ViewFilterContent::Error { inner: error };
|
||||
self_.unfiltered = bytes;
|
||||
self_.notice = Some(format!("{} failed", self_.filter_invocation).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for ViewFilter {
|
||||
fn draw(&mut self, _: &mut CellBuffer, _: Area, _: &mut Context) {}
|
||||
fn process_event(&mut self, event: &mut UIEvent, context: &mut Context) -> bool {
|
||||
if let Some(ref mut f) = self.event_handler {
|
||||
return f(self, event, context);
|
||||
}
|
||||
if let ViewFilterContent::InlineAttachments { ref mut parts, .. } = self.body_text {
|
||||
return parts.iter_mut().any(|p| p.process_event(event, context));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_dirty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_dirty(&mut self, _: bool) {}
|
||||
|
||||
fn id(&self) -> ComponentId {
|
||||
self.id
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
//
|
||||
// meli
|
||||
//
|
||||
// Copyright 2023 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/>.
|
||||
|
||||
//use super::ViewFilter;
|
||||
//use crate::melib::{Attachment, AttachmentBuilder};
|
||||
//use crate::Context;
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_text_plain() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "text/plain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_text_html() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "text/html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_multipart_alternative_plain_and_html() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "text/plain");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_multipart_alternative_empty_plain_and_html() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "text/html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_multipart_digest() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "multipart/digest");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_multipart_mixed() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "multipart/mixed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_view_filter_multipart_related() {
|
||||
println!("[ref:TODO]");
|
||||
//let bytes = b"";
|
||||
//let tempdir = tempfile::tempdir().unwrap();
|
||||
//let mut ctx = Context::new_mock(&tempdir);
|
||||
//let att: Attachment = AttachmentBuilder::new(bytes).build();
|
||||
//let value = ViewFilter::new_attachment(&att, &mut ctx).unwrap();
|
||||
//assert_eq!(&value.content_type.to_string(), "text/related");
|
||||
}
|